package com.backsun.lod.objects; import java.awt.Color; import com.backsun.lod.util.enums.ColorDirection; import com.backsun.lod.util.enums.LodCorner; import com.backsun.lod.util.enums.LodLocation; import net.minecraft.block.Blocks; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.color.BlockColors; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.ChunkSection; /** * This object contains position * and color data for an LOD object. * * @author James Seibel * @version 02-13-2021 */ public class LodChunk { /** how many different pieces of data are in one line */ private static final int DATA_DELIMITER_COUNT = 28; /** This is what separates each piece of data in the toData method */ public static final char DATA_DELIMITER = ','; public static final int WIDTH = 16; private static final int CHUNK_DATA_WIDTH = WIDTH; private static final int CHUNK_DATA_HEIGHT = WIDTH; /** * This is how many blocks are * required at a specific y-value * to constitute a LOD point */ private final int LOD_BLOCK_REQ = 16; // the max number of blocks per layer = 64 (8*8) // since each layer is 1/4 the chunk /** The x coordinate of the chunk. */ public int x; /** The z coordinate of the chunk. */ public int z; // each short is the height of // that 8th of the chunk. public short top[]; public short bottom[]; /** The average color of each 6 cardinal directions */ public Color colors[]; //==============// // constructors // //==============// /** * Create an empty invisible LodChunk at (0,0) */ public LodChunk() { x = 0; z = 0; top = new short[4]; bottom = new short[4]; colors = new Color[6]; // by default have the colors invisible for(ColorDirection dir : ColorDirection.values()) { colors[dir.value] = new Color(0, 0, 0, 0); } } /** * Creates an LodChunk from the string * generated by the toData method. * * @throws IllegalArgumentException if the data isn't valid to create a LodChunk * @throws NumberFormatException if the data can't be converted into an int at any point */ public LodChunk(String data) throws IllegalArgumentException, NumberFormatException { /* * data format: * x, z, top data, bottom data, rgb color data * * example: * 5,8, 4,4,4,4, 0,0,0,0, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255, */ // make sure there are the correct number of entries // in the data string (28) int count = 0; for(int i = 0; i < data.length(); i++) { if(data.charAt(i) == DATA_DELIMITER) { count++; } } if(count != DATA_DELIMITER_COUNT) { throw new IllegalArgumentException("LodChunk constructor givin an invalid string. The data given had " + count + " delimiters when it should have had " + DATA_DELIMITER_COUNT + "."); } // index we will use when going through the String int index = 0; int lastIndex = 0; // x and z position index = data.indexOf(DATA_DELIMITER, 0); x = Integer.parseInt(data.substring(0,index)); lastIndex = index; index = data.indexOf(DATA_DELIMITER, lastIndex + 1); z = Integer.parseInt(data.substring(lastIndex+1,index)); // top top = new short[4]; for(LodLocation loc : LodLocation.values()) { lastIndex = index; index = data.indexOf(DATA_DELIMITER, lastIndex + 1); top[loc.value] = Short.parseShort(data.substring(lastIndex+1,index)); } // bottom bottom = new short[4]; for(LodLocation loc : LodLocation.values()) { lastIndex = index; index = data.indexOf(DATA_DELIMITER, lastIndex + 1); bottom[loc.value] = Short.parseShort(data.substring(lastIndex+1,index)); } // color colors = new Color[6]; for(ColorDirection dir : ColorDirection.values()) { int red = 0; int green = 0; int blue = 0; // get r,g,b for(int i = 0; i < 3; i++) { lastIndex = index; index = data.indexOf(DATA_DELIMITER, lastIndex + 1); String raw = ""; switch(i) { case 0: raw = data.substring(lastIndex+1,index); red = Short.parseShort(raw); break; case 1: raw = data.substring(lastIndex+1,index); green = Short.parseShort(raw); break; case 2: raw = data.substring(lastIndex+1,index); blue = Short.parseShort(raw); break; } } colors[dir.value] = new Color(red, green, blue); } } /** * Creates a LodChunk for a chunk in the given world.
* Note: The world is required to determine each block's color * * @throws IllegalArgumentException * thrown if either the chunk or world is null. */ public LodChunk(Chunk chunk, World world) throws IllegalArgumentException { if(chunk == null) { throw new IllegalArgumentException("LodChunk constructor given a null chunk"); } if(world == null) { throw new IllegalArgumentException("LodChunk constructor given a null world"); } x = chunk.getPos().x; z = chunk.getPos().z; top = new short[4]; bottom = new short[4]; colors = new Color[6]; // generate the top and bottom points of this LOD for(LodLocation loc : LodLocation.values()) { top[loc.value] = generateLodCorner(chunk, SectionGenerationMode.GENERATE_TOP, loc); bottom[loc.value] = generateLodCorner(chunk, SectionGenerationMode.GENERATE_BOTTOM, loc); } // determine the average color for each direction for(ColorDirection dir : ColorDirection.values()) { colors[dir.value] = generateLodColorForDirection(chunk, world, dir); } } //=====================// // constructor helpers // //=====================// /** * Generate the height for the given LodLocation, either the top or bottom. *

* If invalid/null/empty chunks are given * crashes may occur. */ public short generateLodCorner(Chunk chunk, SectionGenerationMode generationMode, LodLocation lodLoc) { // should have a length of 16 // (each storage is 16x16x16 and the // world height is 256) ChunkSection[] chunkSections = chunk.getSections(); int startX = 0; int endX = 0; int startZ = 0; int endZ = 0; // determine where we should look in this // chunk switch(lodLoc) { case NE: // -N startZ = 0; endZ = (CHUNK_DATA_WIDTH / 2) - 1; // +E startX = CHUNK_DATA_WIDTH / 2; endX = CHUNK_DATA_WIDTH - 1; break; case SE: // +S startZ = CHUNK_DATA_WIDTH / 2; endZ = CHUNK_DATA_WIDTH; // +E startX = CHUNK_DATA_WIDTH / 2; endX = CHUNK_DATA_WIDTH; break; case SW: // +S startZ = CHUNK_DATA_WIDTH / 2; endZ = CHUNK_DATA_WIDTH; // -W startX = 0; endX = (CHUNK_DATA_WIDTH / 2) - 1; break; case NW: // -N startZ = 0; endZ = CHUNK_DATA_WIDTH / 2; // -W startX = 0; endX = CHUNK_DATA_WIDTH / 2; break; } if(generationMode == SectionGenerationMode.GENERATE_TOP) return determineTopPoint(chunkSections, startX, endX, startZ, endZ); else return determineBottomPoint(chunkSections, startX, endX, startZ, endZ); } /** GENERATE_TOP, GENERATE_BOTTOM */ private enum SectionGenerationMode { GENERATE_TOP, GENERATE_BOTTOM; } /** * Find the lowest valid point from the bottom. */ private short determineBottomPoint(ChunkSection[] chunkSections, int startX, int endX, int startZ, int endZ) { // search from the bottom up for(int i = 0; i < chunkSections.length; i++) { for(int y = 0; y < CHUNK_DATA_HEIGHT; y++) { if(isLayerValidLodPoint(chunkSections, startX, endX, startZ, endZ, i, y)) { // we found // enough blocks in this // layer to count as an // LOD point return (short) (y + (i * CHUNK_DATA_HEIGHT)); } } } // we never found a valid LOD point return -1; } /** * Find the highest valid point from the Top */ private short determineTopPoint(ChunkSection[] chunkSections, int startX, int endX, int startZ, int endZ) { // search from the top down for(int i = chunkSections.length - 1; i >= 0; i--) { for(int y = CHUNK_DATA_WIDTH - 1; y >= 0; y--) { if(isLayerValidLodPoint(chunkSections, startX, endX, startZ, endZ, i, y)) { // we found // enough blocks in this // layer to count as an // LOD point return (short) (y + (i * CHUNK_DATA_HEIGHT)); } } } // we never found a valid LOD point return -1; } /** * Is the layer between the given X, Z, and dataIndex * values a valid LOD point? */ private boolean isLayerValidLodPoint( ChunkSection[] chunkSections, int startX, int endX, int startZ, int endZ, int sectionIndex, int y) { // search through this layer int layerBlocks = 0; for(int x = startX; x < endX; x++) { for(int z = startZ; z < endZ; z++) { if(chunkSections[sectionIndex] == null) { // this section doesn't have any blocks, // it is not a valid section return false; } else { if(chunkSections[sectionIndex].getBlockState(x, y, z) != null && chunkSections[sectionIndex].getBlockState(x, y, z).getBlock() != Blocks.AIR) { // we found a valid block in // in this layer layerBlocks++; if(layerBlocks >= LOD_BLOCK_REQ) { return true; } } } } // z } // x return false; } /** * Generate the color of the given ColorDirection at the given chunk * in the given world. */ private Color generateLodColorForDirection(Chunk chunk, World world, ColorDirection colorDir) { Minecraft mc = Minecraft.getInstance(); BlockColors bc = mc.getBlockColors(); switch (colorDir) { case TOP: return generateLodColorVertical(chunk, colorDir, world, bc); case BOTTOM: return generateLodColorVertical(chunk, colorDir, world, bc); case N: return generateLodColorHorizontal(chunk, colorDir, world, bc); case S: return generateLodColorHorizontal(chunk, colorDir, world, bc); case E: return generateLodColorHorizontal(chunk, colorDir, world, bc); case W: return generateLodColorHorizontal(chunk, colorDir, world, bc); } return new Color(0, 0, 0, 0); } /** * Generates the color of the top or bottom of a given chunk in the given world. * * @throws IllegalArgumentException if given a ColorDirection other than TOP or BOTTOM */ private Color generateLodColorVertical(Chunk chunk, ColorDirection colorDir, World world, BlockColors bc) { if(colorDir != ColorDirection.TOP && colorDir != ColorDirection.BOTTOM) { throw new IllegalArgumentException("generateLodColorVertical only accepts the ColorDirection TOP or BOTTOM"); } ChunkSection[] chunkSections = chunk.getSections(); int numbOfBlocks = 0; int red = 0; int green = 0; int blue = 0; boolean goTopDown = (colorDir == ColorDirection.TOP); // either go top down or bottom up int dataStart = goTopDown? chunkSections.length - 1 : 0; int dataMax = chunkSections.length; int dataMin = 0; int dataIncrement = goTopDown? -1 : 1; int topStart = goTopDown? CHUNK_DATA_HEIGHT - 1 : 0; int topMax = CHUNK_DATA_HEIGHT; int topMin = 0; int topIncrement = goTopDown? -1 : 1; for(int x = 0; x < CHUNK_DATA_WIDTH; x++) { for(int z = 0; z < CHUNK_DATA_WIDTH; z++) { boolean foundBlock = false; for(int i = dataStart; !foundBlock && i >= dataMin && i < dataMax; i += dataIncrement) { if(!foundBlock && chunkSections[i] != null) { for(int y = topStart; !foundBlock && y >= topMin && y < topMax; y += topIncrement) { int ci; ci = chunkSections[i].getBlockState(x, y, z).materialColor.colorValue; if(ci == 0) { // skip air or invisible blocks continue; } Color c = intToColor(ci); red += c.getRed(); green += c.getGreen(); blue += c.getBlue(); numbOfBlocks++; // we found a valid block, skip to the // next x and z foundBlock = true; } } } } } if(numbOfBlocks == 0) numbOfBlocks = 1; red /= numbOfBlocks; green /= numbOfBlocks; blue /= numbOfBlocks; return new Color(red, green, blue); } /** * Generates the color of the side of a given chunk in the given world for the given ColorDirection. * * @throws IllegalArgumentException if given a ColorDirection other than N, S, W, E (North, South, East, West) */ private Color generateLodColorHorizontal(Chunk chunk, ColorDirection colorDir, World world, BlockColors bc) { if(colorDir != ColorDirection.N && colorDir != ColorDirection.S && colorDir != ColorDirection.E && colorDir != ColorDirection.W) { throw new IllegalArgumentException("generateLodColorHorizontal only accepts the ColorDirection N (North), S (South), E (East), or W (West)"); } ChunkSection[] chunkSections = chunk.getSections(); int numbOfBlocks = 0; int red = 0; int green = 0; int blue = 0; // these don't change since the over direction doesn't matter int overStart = 0; int overIncrement = 1; // determine which direction is "in" int inStart = 0; int inIncrement = 1; switch (colorDir) { case N: inStart = 0; inIncrement = 1; break; case S: inStart = CHUNK_DATA_WIDTH - 1; inIncrement = -1; break; case E: inStart = 0; inIncrement = 1; break; case W: inStart = CHUNK_DATA_WIDTH - 1; inIncrement = -1; break; default: // we were given an invalid position, return invisible. // this shouldn't happen and is mostly here to make the // compiler happy return new Color(0,0,0,0); } for (int i = 0; i < chunkSections.length; i++) { if (chunkSections[i] != null) { for (int y = 0; y < CHUNK_DATA_HEIGHT; y++) { boolean foundBlock = false; // over moves "over" the side of the chunk // in moves "into" the chunk until it finds a block for (int over = overStart; !foundBlock && over >= 0 && over < CHUNK_DATA_WIDTH; over += overIncrement) { for (int in = inStart; !foundBlock && in >= 0 && in < CHUNK_DATA_WIDTH; in += inIncrement) { int x = -1; int z = -1; // determine which should be X and Z switch(colorDir) { case N: x = over; z = in; break; case S: x = over; z = in; break; case E: x = in; z = over; break; case W: x = in; z = over; break; default: // this will never happen, it would have // been caught by the switch before the loops break; } int ci; ci = chunkSections[i].getBlockState(x, y, z).getMaterial().getColor().colorValue; if (ci == 0) { // skip air or invisible blocks continue; } Color c = intToColor(ci); red += c.getRed(); green += c.getGreen(); blue += c.getBlue(); numbOfBlocks++; // we found a valid block, skip to the // next x and z foundBlock = true; } } } } } if(numbOfBlocks == 0) numbOfBlocks = 1; red /= numbOfBlocks; green /= numbOfBlocks; blue /= numbOfBlocks; return new Color(red, green, blue); } /** * Convert a BlockColors int into a Color object. */ private Color intToColor(int num) { int filter = 0b11111111; int red = (num >> 16 ) & filter; int green = (num >> 8 ) & filter; int blue = num & filter; return new Color(red, green, blue); } /** * Convert a Color into a BlockColors object. */ @SuppressWarnings("unused") private int colorToInt(Color color) { return color.getRGB(); } //================// // misc functions // //================// /** * If this LOD is either invisible from every * direction or doesn't have a valid height * it is empty. */ public boolean isLodEmpty() { for(LodCorner corner : LodCorner.values()) if(top[corner.value] != -1 || bottom[corner.value] != -1) // at least one corner is valid return false; Color invisible = new Color(0,0,0,0); for(ColorDirection dir : ColorDirection.values()) if(!colors[dir.value].equals(invisible)) // at least one direction has a non-invisible color return false; return true; } //========// // output // //========// /** * Outputs all data in csv format * with the given delimiter. *
* Exports data in the form: *
* x, z, top data, bottom data, rgb color data * *
* example output: *
* 5,8, 4,4,4,4, 0,0,0,0 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255, */ public String toData() { String s = ""; s += Integer.toString(x) + DATA_DELIMITER + Integer.toString(z) + DATA_DELIMITER; for(int i = 0; i < top.length; i++) { s += Short.toString(top[i]) + DATA_DELIMITER; } for(int i = 0; i < bottom.length; i++) { s += Short.toString(bottom[i]) + DATA_DELIMITER; } for(int i = 0; i < colors.length; i++) { s += Integer.toString(colors[i].getRed()) + DATA_DELIMITER + Integer.toString(colors[i].getGreen()) + DATA_DELIMITER + Integer.toString(colors[i].getBlue()) + DATA_DELIMITER; } return s; } @Override public String toString() { String s = ""; s += "x: " + x + " z: " + z + "\t"; s += "(" + colors[ColorDirection.TOP.value].getRed() + ", " + colors[ColorDirection.TOP.value].getGreen() + ", " + colors[ColorDirection.TOP.value].getBlue() + ")"; return s; } }