diff --git a/src/main/java/com/seibel/lod/builders/LodNodeBuilder.java b/src/main/java/com/seibel/lod/builders/LodNodeBuilder.java new file mode 100644 index 000000000..5f2d00988 --- /dev/null +++ b/src/main/java/com/seibel/lod/builders/LodNodeBuilder.java @@ -0,0 +1,172 @@ +package com.seibel.lod.builders; + +import com.seibel.lod.enums.LodDetail; +import com.seibel.lod.handlers.LodConfig; +import com.seibel.lod.objects.LodChunk; +import com.seibel.lod.objects.LodDataPoint; +import com.seibel.lod.objects.LodDimension; +import com.seibel.lod.objects.LodWorld; +import com.seibel.lod.objects.quadTree.LodNodeData; +import com.seibel.lod.objects.quadTree.LodQuadTreeWorld; +import kaptainwutax.biomeutils.source.BiomeSource; +import kaptainwutax.biomeutils.source.OverworldBiomeSource; +import kaptainwutax.mcutils.state.Dimension; +import kaptainwutax.mcutils.version.MCVersion; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.DimensionType; +import net.minecraft.world.IWorld; +import net.minecraft.world.chunk.IChunk; + +import java.awt.*; +import java.util.Iterator; +import java.util.OptionalLong; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class LodNodeBuilder { + private ExecutorService lodGenThreadPool = Executors.newSingleThreadExecutor(); + private long seed; + private DimensionType dimension; + + /** Default size of any LOD regions we use */ + public int regionWidth = 5; + + + /** fast biome calculator */ + private BiomeSource biomeSource; + //Biome biome=biomeSource.getBiome(x,y,z); // here y is always 0 no matter what you pass + + public LodNodeBuilder(){ + + } + public setApproxGenerator(long seed){ + //Dimension.OVERWORLD; + //Dimension.END; + //Dimension.NETHER; + biomeSource = BiomeSource.of(Dimension.OVERWORLD ,MCVersion.v1_16_4, seed); + } + + public void generateLodNodeAsync(List dataList){ + Thread thread = new Thread(() ->{ + for(LodNodeData data : dataList){ + + } + }); + thread.setPriority(4); + lodGenThreadPool.execute(thread); + + return; + } + + public void generateLodNodeAsync(IChunk chunk, LodWorld lodWorld, IWorld world) + { + if (lodWorld == null || !lodWorld.getIsWorldLoaded()) + return; + + // don't try to create an LOD object + // if for some reason we aren't + // given a valid chunk object + if (chunk == null) + return; + + Thread thread = new Thread(() -> + { + try + { + DimensionType dim = world.getDimensionType(); + + LodChunk lod = generateLodFromChunk(chunk, config); + + LodDimension lodDim; + + if (lodWorld.getLodDimension(dim) == null) + { + lodDim = new LodDimension(dim, lodWorld, regionWidth); + lodWorld.addLodDimension(lodDim); + } + else + { + lodDim = lodWorld.getLodDimension(dim); + } + + lodDim.addLod(lod); + } + catch(IllegalArgumentException | NullPointerException e) + { + // if the world changes while LODs are being generated + // they will throw errors as they try to access things that no longer + // exist. + } + }); + lodGenThreadPool.execute(thread); + + return; + } + + + + + + /** + * Creates a LodChunk for a chunk in the given world. + * + * @throws IllegalArgumentException + * thrown if either the chunk or world is null. + */ + public LodChunk generateLodFromChunk(IChunk chunk) throws IllegalArgumentException + { + return generateLodFromChunk(chunk, new LodBuilderConfig()); + } + + /** + * Creates a LodChunk for a chunk in the given world. + * + * @throws IllegalArgumentException + * thrown if either the chunk or world is null. + */ + public LodChunk generateLodFromChunk(IChunk chunk, LodBuilderConfig config) throws IllegalArgumentException + { + if(chunk == null) + throw new IllegalArgumentException("generateLodFromChunk given a null chunk"); + + + LodDetail detail = LodConfig.CLIENT.lodDetail.get(); + LodDataPoint[][] dataPoints = new LodDataPoint[detail.lengthCount][detail.lengthCount]; + + for(int i = 0; i < detail.lengthCount * detail.lengthCount; i++) + { + int startX = detail.startX[i]; + int startZ = detail.startZ[i]; + int endX = detail.endX[i]; + int endZ = detail.endZ[i]; + + Color color; + + color = generateLodColorForArea(chunk, config, startX, startZ, endX, endZ); + + + short height; + short depth; + + if (!config.useHeightmap) + { + height = determineHeightPointForArea(chunk.getSections(), startX, startZ, endX, endZ); + depth = determineBottomPointForArea(chunk.getSections(), startX, startZ, endX, endZ); + } + else + { + height = determineHeightPoint(chunk.getHeightmap(LodChunk.DEFAULT_HEIGHTMAP), startX, startZ, endX, endZ); + depth = 0; + } + + int x = i / detail.lengthCount; + int z = i % detail.lengthCount; + + dataPoints[x][z] = new LodDataPoint(height, depth, color); + } + + return new LodChunk(chunk.getPos(), dataPoints, detail); + } + + +} diff --git a/src/main/java/com/seibel/lod/handlers/LodQuadTreeDimensionFileHandler.java b/src/main/java/com/seibel/lod/handlers/LodQuadTreeDimensionFileHandler.java new file mode 100644 index 000000000..9ab9af696 --- /dev/null +++ b/src/main/java/com/seibel/lod/handlers/LodQuadTreeDimensionFileHandler.java @@ -0,0 +1,324 @@ +package com.seibel.lod.handlers; + +import com.seibel.lod.objects.quadTree.LodNodeData; +import com.seibel.lod.objects.quadTree.LodQuadTree; +import com.seibel.lod.objects.quadTree.LodQuadTreeDimension; +import com.seibel.lod.proxy.ClientProxy; + +import java.io.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class LodQuadTreeDimensionFileHandler { + /** This is what separates each piece of data */ + public static final char DATA_DELIMITER = ','; + + + private LodQuadTreeDimension loadedDimension = null; + public long regionLastWriteTime[][]; + + private File dimensionDataSaveFolder; + + /** lod */ + private final String FILE_NAME_PREFIX = "lod"; + /** .txt */ + private final String FILE_EXTENSION = ".txt"; + + /** This is the file version currently accepted by this + * file handler, older versions (smaller numbers) will be deleted and overwritten, + * newer versions (larger numbers) will be ignored and won't be read. */ + public static final int LOD_SAVE_FILE_VERSION = 2; + + /** This is the string written before the file version */ + private static final String LOD_FILE_VERSION_PREFIX = "lod_save_file_version"; + + /** Allow saving asynchronously, but never try to save multiple regions + * at a time */ + private ExecutorService fileWritingThreadPool = Executors.newSingleThreadExecutor(); + + + public LodQuadTreeDimensionFileHandler(File newSaveFolder, LodQuadTreeDimension newLoadedDimension) + { + if (newSaveFolder == null) + throw new IllegalArgumentException("LodDimensionFileHandler requires a valid File location to read and write to."); + + dimensionDataSaveFolder = newSaveFolder; + + loadedDimension = newLoadedDimension; + // these two variable are used in sync with the LodDimension + regionLastWriteTime = new long[loadedDimension.getWidth()][loadedDimension.getWidth()]; + for(int i = 0; i < loadedDimension.getWidth(); i++) + for(int j = 0; j < loadedDimension.getWidth(); j++) + regionLastWriteTime[i][j] = -1; + } + + + + + + //================// + // read from file // + //================// + + + + /** + * Return the LodQuadTree region at the given coordinates. + * (null if the file doesn't exist) + */ + public LodQuadTree loadRegionFromFile(int regionX, int regionZ) + { + + String fileName = getFileNameAndPathForRegion(regionX, regionZ); + + File f = new File(fileName); + + if (!f.exists()) + { + // there wasn't a file, don't + // return anything + return null; + } + + List dataList = new ArrayList<>(); + try + { + BufferedReader br = new BufferedReader(new FileReader(f)); + String s = br.readLine(); + int fileVersion = -1; + if(s != null && !s.isEmpty()) + { + // try to get the file version + try + { + fileVersion = Integer.parseInt(s.substring(s.indexOf(' ')).trim()); + } + catch(NumberFormatException | StringIndexOutOfBoundsException e) + { + // this file doesn't have a version + // keep the version as -1 + fileVersion = -1; + } + + // check if this file can be read by this file handler + if(fileVersion < LOD_SAVE_FILE_VERSION) + { + // the file we are reading is an older version, + // close the reader and delete the file. + br.close(); + f.delete(); + ClientProxy.LOGGER.info("Outdated LOD region file for region: (" + regionX + "," + regionZ + ") version: " + fileVersion + + ", version requested: " + LOD_SAVE_FILE_VERSION + + " File was been deleted."); + + return null; + } + else if(fileVersion > LOD_SAVE_FILE_VERSION) + { + // the file we are reading is a newer version, + // close the reader and ignore the file, we don't + // want to accidently delete anything the user may want. + br.close(); + ClientProxy.LOGGER.info("Newer LOD region file for region: (" + regionX + "," + regionZ + ") version: " + fileVersion + + ", version requested: " + LOD_SAVE_FILE_VERSION + + " this region will not be written to in order to protect the newer file."); + + return null; + } + } + else + { + // there is no data in this file + br.close(); + return null; + } + + + // this file is a readable version, begin reading the file + s = br.readLine(); + + while(s != null && !s.isEmpty()) + { + try + { + dataList.add(new LodNodeData(s)); + } + catch(IllegalArgumentException e) + { + // we were unable to create this chunk + // for whatever reason. + // skip to the next chunk + ClientProxy.LOGGER.warn(e.getMessage()); + } + + s = br.readLine(); + } + br.close(); + } + catch (IOException e) + { + // the buffered reader encountered a + // problem reading the file + return null; + } + return new LodQuadTree(dataList,regionX, regionZ); + } + + + + + + + + + //==============// + // Save to File // + //==============// + + /** + * Save all dirty regions in this LodDimension to file. + */ + public void saveDirtyRegionsToFileAsync() + { + fileWritingThreadPool.execute(saveDirtyRegionsThread); + } + + private Thread saveDirtyRegionsThread = new Thread(() -> + { + for(int i = 0; i < loadedDimension.getWidth(); i++) + { + for(int j = 0; j < loadedDimension.getWidth(); j++) + { + if(loadedDimension.isRegionDirty[i][j] && loadedDimension.regions[i][j] != null) + { + saveRegionToDisk(loadedDimension.regions[i][j]); + loadedDimension.isRegionDirty[i][j] = false; + } + } + } + }); + + /** + * Save a specific region to disk.
+ * Note:
+ * 1. If a file already exists for a newer version + * the file won't be written.
+ * 2. This will save to the LodDimension that this + * handler is associated with. + */ + private void saveRegionToDisk(LodQuadTree region) + { + // convert chunk coordinates to region + // coordinates + int x = region.getLodNodeData().posX; + int z = region.getLodNodeData().posX; + + File f = new File(getFileNameAndPathForRegion(x, z)); + + try + { + // make sure the file and folder exists + if (!f.exists()) + { + // the file doesn't exist, + // create it and the folder if need be + if(!f.getParentFile().exists()) + f.getParentFile().mkdirs(); + f.createNewFile(); + } + else + { + // the file exists, make sure it + // is the correct version. + // (to make sure we don't overwrite a newer + // version file if it exists) + + BufferedReader br = new BufferedReader(new FileReader(f)); + String s = br.readLine(); + int fileVersion = LOD_SAVE_FILE_VERSION; + + if(s != null && !s.isEmpty()) + { + // try to get the file version + try + { + fileVersion = Integer.parseInt(s.substring(s.indexOf(' ')).trim()); + } + catch(NumberFormatException | StringIndexOutOfBoundsException e) + { + // this file doesn't have a correctly formated version + // just overwrite the file + } + } + br.close(); + + // check if this file can be written to by the file handler + if(fileVersion <= LOD_SAVE_FILE_VERSION) + { + // we are good to continue and overwrite the old file + } + else //if(fileVersion > LOD_SAVE_FILE_VERSION) + { + // the file we are reading is a newer version, + // don't write anything, we don't want to accidently + // delete anything the user may want. + return; + } + } + + FileWriter fw = new FileWriter(f); + + // add the version of this file + fw.write(LOD_FILE_VERSION_PREFIX + " " + LOD_SAVE_FILE_VERSION + "\n"); + + // add each LodChunk to the file + for(LodNodeData lodNodeData : Collections.unmodifiableList(region.getNodeList(false, true, true))) { + fw.write(lodNodeData.toData() + "\n"); + lodNodeData.dirty = false; + } + fw.close(); + } + catch(Exception e) + { + ClientProxy.LOGGER.error("LOD file write error: " + e.getMessage()); + } + } + + + + + + + + //================// + // helper methods // + //================// + + + /** + * Return the name of the file that should contain the + * region at the given x and z.
+ * Returns null if this object isn't ready to read and write.

+ * + * example: "lod.FULL.0.0.txt" + */ + private String getFileNameAndPathForRegion(int regionX, int regionZ) + { + try + { + // saveFolder is something like + // ".\Super Flat\DIM-1\data" + // or + // ".\Super Flat\data" + return dimensionDataSaveFolder.getCanonicalPath() + File.separatorChar + + FILE_NAME_PREFIX + regionX + "." + regionZ + FILE_EXTENSION; + } + catch(IOException e) + { + return null; + } + } +} diff --git a/src/main/java/com/seibel/lod/objects/quadTree/LodNodeData.java b/src/main/java/com/seibel/lod/objects/quadTree/LodNodeData.java new file mode 100644 index 000000000..b354ee826 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/quadTree/LodNodeData.java @@ -0,0 +1,246 @@ +package com.seibel.lod.objects.quadTree; + +import com.seibel.lod.handlers.LodQuadTreeDimensionFileHandler; + +import java.awt.*; +import java.util.*; +import java.util.List; + +public class LodNodeData { + /** This is what separates each piece of data in the toData method */ + private static final char DATA_DELIMITER = LodQuadTreeDimensionFileHandler.DATA_DELIMITER; + + + + /** this is how many pieces of data are exported when toData is called */ + public static final int NUMBER_OF_DELIMITERS = 9; + + private static final Color INVISIBLE = new Color(0,0,0,0); + + //level height goes from 0 to 9 with 0 the deepest (block size) and 9 the highest (region size) + public final byte level; + public static final byte REGION_LEVEL = 9; //at level 9 we reach the dimension of a single region + public static final byte CHUNK_LEVEL = 4; //at level 4 we reach the dimension of a single chunk + public static final byte BLOCK_LEVEL = 0; //at level 0 we reach the dimension of a single block + + //indicate the width in block of this node (goes from 1 to 512) + public final short width; + public static final short REGION_WIDTH = 512; //at level 9 we reach the dimension of a single region + public static final short CHUNK_WIDTH = 16; //at level 4 we reach the dimension of a single chunk + public static final short BLOCK_WIDTH = 1; //at level 0 we reach the dimension of a single block + + //this 2 values indicate the position of the LOD in the relative Level + //this will be useful in the generation process + public final int posX; + public final int posZ; + + //these 4 value indicate the corner of the LOD block + //they can be named SW, SE, NW, NE as the cardinal direction. + //the start values should always be smaller than the end values. + //All this value could be calculated from level and levelWidth + //so they could be removed and replaced with just a getter + public final int startX; + public final int startZ; + public final int endX; + public final int endZ; + //these 2 value indicate the center of the LodNode in real coordinate. This + //can be used to calculate the distance from the player + public final int centerX; + public final int centerZ; + + /** highest point */ + public short height; + + /** lowest point */ + public short depth; + + /** The average color for the 6 cardinal directions */ + public Color color; + + public boolean real; + public boolean voidNode; + //if dirty is true, then this node have unsaved changes + public boolean dirty; + + /** + * Creates and empty LodDataPoint + * This LodDataPoint only contains the position data + * @param level of the node + * @param posX position x in the level + * @param posZ posizion z in the level + */ + public LodNodeData(byte level, int posX, int posZ){ + this.level = level; + this.posX = posX; + this.posZ = posZ; + width = (short) Math.pow(2, level); + startX = posX * width; + startZ = posZ * width; + endX = startX + width - 1; + endZ = startZ + width - 1; + centerX = startX + width/2; + centerZ = startZ + width/2; + height = -1; + depth = -1; + color = INVISIBLE; + real = false; + dirty = true; + voidNode = true; + } + + /** + * Constructor for a LodNodeData + * @param level level of this + * @param posX + * @param posZ + * @param height + * @param depth + * @param color + * @param real + */ + public LodNodeData(byte level, int posX, int posZ, short height, short depth, Color color, boolean real){ + this.level = level; + this.posX = posX; + this.posZ = posZ; + width = (short) Math.pow(2, level); + startX = posX * width; + startZ = posZ * width; + endX = startX + width - 1; + endZ = startZ + width - 1; + centerX = startX + width/2; + centerZ = startZ + width/2; + this.height = height; + this.depth = depth; + this.color = color; + this.real = real; + dirty = true; + voidNode = false; + } + + public LodNodeData(byte level, int posX, int posZ, int height, int depth, Color color, boolean real) { + this(level, posX, posZ, (short) height,(short) depth, color, real); + } + + public LodNodeData(String data) + { + int index = 0; + int lastIndex = 0; + + index = data.indexOf(DATA_DELIMITER, 0); + this.level = (byte) Integer.parseInt(data.substring(0,index)); + + lastIndex = index; + index = data.indexOf(DATA_DELIMITER, lastIndex+1); + this.posX = Integer.parseInt(data.substring(lastIndex+1,index)); + + lastIndex = index; + index = data.indexOf(DATA_DELIMITER, lastIndex+1); + this.posZ = Integer.parseInt(data.substring(lastIndex+1,index)); + + lastIndex = index; + index = data.indexOf(DATA_DELIMITER, lastIndex+1); + this.height = (short) Integer.parseInt(data.substring(lastIndex+1,index)); + + lastIndex = index; + index = data.indexOf(DATA_DELIMITER, lastIndex+1); + this.depth = (short) Integer.parseInt(data.substring(lastIndex+1,index)); + + lastIndex = index; + index = data.indexOf(DATA_DELIMITER, lastIndex+1); + int r = Integer.parseInt(data.substring(lastIndex+1,index)); + lastIndex = index; + index = data.indexOf(DATA_DELIMITER, lastIndex+1); + int g = Integer.parseInt(data.substring(lastIndex+1,index)); + lastIndex = index; + index = data.indexOf(DATA_DELIMITER, lastIndex+1); + int b = Integer.parseInt(data.substring(lastIndex+1,index)); + lastIndex = index; + index = data.indexOf(DATA_DELIMITER, lastIndex+1); + int a = Integer.parseInt(data.substring(lastIndex+1,index)); + this.color = new Color(r,g,b,a); + + int val = Integer.parseInt(data.substring(lastIndex+1,index)); + this.real = (val == 1); + width = (short) Math.pow(2, level); + + val = Integer.parseInt(data.substring(lastIndex+1,index)); + this.voidNode = (val == 1); + startX = posX * width; + startZ = posZ * width; + endX = startX + width - 1; + endZ = startZ + width - 1; + centerX = startX + width/2; + centerZ = startZ + width/2; + dirty = false; + } + + public void update(LodNodeData lodNodeData){ + this.height = lodNodeData.height; + this.depth = lodNodeData.depth; + this.color = lodNodeData.color; + this.real = lodNodeData.real; + dirty = true; + } + + public void combineData(List dataList){ + if(dataList.isEmpty()){ + height = -1; + depth = -1; + color = INVISIBLE; + }else { + short height = (short) dataList.stream().mapToInt(x -> (int) x.height).min().getAsInt(); + short depth = (short) dataList.stream().mapToInt(x -> (int) x.depth).max().getAsInt(); + height = height; + depth = depth; + color = dataList.get(0).color; + real = dataList.stream().filter(x -> x.real).count() == 4; + voidNode = dataList.stream().filter(x -> !x.voidNode).count() == 0; + } + dirty = true; + } + + + + public int hashCode(){ + return Objects.hash(this.real, this.level, this.posX, this.posZ, this.color, this.real, this.voidNode); + } + + + public boolean equals(LodNodeData other){ + return (this.real == other.real + && this.level == other.level + && this.posX == other.posX + && this.posZ == other.posZ + && this.color.equals(other.color) + && this.real == other.real + && this.voidNode == other.voidNode); + } + + + /** + * Outputs all data in a csv format + */ + public String toData(){ + String s = Integer.toString(level) + DATA_DELIMITER + + posX + DATA_DELIMITER + + posZ + DATA_DELIMITER + + Integer.toString(height) + DATA_DELIMITER + + Integer.toString(depth) + DATA_DELIMITER + + color.getRed() + DATA_DELIMITER + + color.getGreen() + DATA_DELIMITER + + color.getBlue() + DATA_DELIMITER + + color.getAlpha() + DATA_DELIMITER; + int val = real ? 1 : 0; + s += val + DATA_DELIMITER; + val = voidNode ? 1 : 0; + s += val + DATA_DELIMITER; + return s; + } + + + + public String toString() + { + return this.toData(); + } +} diff --git a/src/main/java/com/seibel/lod/objects/quadTree/LodQuadTree.java b/src/main/java/com/seibel/lod/objects/quadTree/LodQuadTree.java new file mode 100644 index 000000000..9202b1c7c --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/quadTree/LodQuadTree.java @@ -0,0 +1,363 @@ +package com.seibel.lod.objects.quadTree; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; + +/** + * This object contains all data useful to render LodBlock in a region (32x32 chunk o 512x512 block) + * for every node it contains the border of the block, the size, the position at it's level, the color, the height and the depth. + */ +public class LodQuadTree { + //notes + //The term node correspond to a LodQuadTree object + + + /* + Example on how it will be rendered (the number correspond to the level in the LodNodeData) + .___.___._______._______________. + |6|6| 7 | | | + |6|6|___| 8 | | + | 7 | 7 | | | + |___|___|_______| 9 | + | | | | + | 8 | 8 | | + | | | | + |_______|_______|_______________| + | | | + | | | + | | | + | 9 | 9 | + | | | + | | | + | | | + |_______________|_______________| + */ + //data useful to render + //if children are present then lodNodeData should be a combination of the lodData of the child. This can be + //turned off by deselecting the recursive update in all update method. + private final LodNodeData lodNodeData; + /* + .____.____. + | NW | NE | | + |____|____| Z + | SW | SE | | + |____|____| V + -----X----> + + North - negative z + South - positive z + West - negative x + east - positive x + */ + + + //level completed is true if and only if all child are not null + private boolean nodeFull; + private boolean nodeEmpty; + + //the four child based on the four diagonal cardinal direction + //the first index is for N and S and the second index is for W and S + //children should always be null for level 0. + private final LodQuadTree[][] children; + + //parent should always be null for level 9, and always not null for other levels. + private final LodQuadTree parent; + + /** + * Constructor for level 0 without LodNodeData (region level constructor) + * + * @param regionX indicate the x region position of the node + * @param regionZ indicate the z region position of the node + */ + //maybe the use of useLevelCoordinate could be changed. I could use a builder to do all this work. + public LodQuadTree(int regionX, int regionZ) { + this(null, new LodNodeData(LodNodeData.REGION_LEVEL, regionX, regionZ)); + } + + /** + * Constructor for generic level without LodNodeData + * + * @param parent parent of this node + * @param level level of this note + * @param posX position x in the level + * @param posZ position z in the level + */ + public LodQuadTree(LodQuadTree parent, byte level, int posX, int posZ) { + this(parent, new LodNodeData(level, posX, posZ)); + } + + /** + * Constructor for generic level via the LodNodeData + * + * @param lodNodeData object containing all the information of this node + */ + public LodQuadTree(LodQuadTree parent, LodNodeData lodNodeData) { + this.parent = parent; + this.lodNodeData = lodNodeData; + this.children = new LodQuadTree[2][2]; + this.nodeEmpty = true; + this.nodeFull = false; + } + + /** + * Constructor using a dataList + * + * @param dataList list of LodNodeData to put in this LodQuadTree + * @param regionX x region coordinate + * @param regionZ z region coordinate + */ + public LodQuadTree(List dataList, int regionX, int regionZ) { + this(null, new LodNodeData(LodNodeData.REGION_LEVEL, regionX, regionZ)); + this.setNodesAtLowerLevel(dataList, true); + } + + + /** + * @param dataList list of data to put in the node + * @param updateHigherLevel will update the color and height of higher level only if true + */ + public void setNodesAtLowerLevel(List dataList, boolean updateHigherLevel) { + for (LodNodeData lodNodeData : dataList) { + //this is slow, you could set update to false and use an only top down update method. + this.setNodeAtLowerLevel(lodNodeData, updateHigherLevel); + } + } + + /** + * @param newLodNodeData data to put in the node + * @param updateHigherLevel will update the color and height of higher level only if true + * @return true only if the QuadTree has been changed + */ + public boolean setNodeAtLowerLevel(LodNodeData newLodNodeData, boolean updateHigherLevel) { + //check if we try to introduce a level that is higher or equal than the current one + byte targetLevel = newLodNodeData.level; + byte currentLevel = lodNodeData.level; + if (targetLevel < currentLevel) { + int posX = newLodNodeData.posX; + int posZ = newLodNodeData.posZ; + short widthRatio = (short) (lodNodeData.width / newLodNodeData.width); + int NS = (posX / widthRatio) % lodNodeData.posX; + int WE = (posZ / widthRatio) % lodNodeData.posZ; + if (getChild(NS, WE) == null) { + setChild(NS, WE); + } + LodQuadTree child = getChild(NS, WE); + if (!newLodNodeData.real && child.isNodeReal()) { + return false; + } else { + if (targetLevel == currentLevel - 1) { + child.setLodNodeData(lodNodeData, updateHigherLevel); + return true; + } else { + return child.setNodeAtLowerLevel(lodNodeData, updateHigherLevel); + } + } + } else { + return false; + } + + } + + public LodQuadTree getChild(int NS, int WE) { + return children[NS][WE]; + } + + /** + * setChild will put a child with given data in the given position + * + * @param newLodNodeData data to put in the child + * @param NS North-South position + * @param WE West-East position + */ + public void setChild(LodNodeData newLodNodeData, int NS, int WE) { + if (newLodNodeData.level == lodNodeData.level - 1) { + children[NS][WE] = new LodQuadTree(this, lodNodeData); + } + } + + /** + * setChild will put a child with given data in the given position + * + * @param newLodNodeData data to put in the child + */ + public void setChild(LodNodeData newLodNodeData) { + if (newLodNodeData.level == lodNodeData.level - 1) { + int NS = newLodNodeData.posX % lodNodeData.posX; + int WE = newLodNodeData.posZ % lodNodeData.posZ; + children[NS][WE] = new LodQuadTree(this, lodNodeData); + } + } + + /** + * setChild will put a child in the given position + * + * @param NS North-South position + * @param WE West-East position + */ + public void setChild(int NS, int WE) { + int childX = lodNodeData.posX * 2 + WE; + int childZ = lodNodeData.posZ * 2 + NS; + children[NS][WE] = new LodQuadTree(this, (byte) (lodNodeData.level - 1), childX, childZ); + } + + /** + * Update level update the level data such as levelFull and lodNodeData. + * + * @param recursiveUpdate if recursive is true the update will rise up to the level 0 + */ + private void updateLevel(boolean recursiveUpdate) { + boolean isFull = true; + boolean isEmpty = true; + List dataList = new ArrayList<>(); + for (int NS = 0; NS <= 1; NS++) { + for (int WE = 0; WE <= 1; WE++) { + if (children[NS][WE] != null) { + dataList.add(children[NS][WE].getLodNodeData()); + isEmpty = false; + } else { + isFull = false; + } + } + } + nodeFull = isFull; + nodeEmpty = isEmpty; + lodNodeData.combineData(dataList); + if (lodNodeData.level > 0 && recursiveUpdate) { + this.parent.updateLevel(recursiveUpdate); + } + } + + /** + * method to get certain nodes from the LodQuadTree + * + * @param getOnlyReal if true it will return only real nodes + * @param getOnlyDirty if true it will return only dirty nodes + * @param getOnlyLeaf if true it will return only leaf nodes + * @return list of nodes + */ + public List getNodeList(boolean getOnlyReal, boolean getOnlyDirty, boolean getOnlyLeaf) { + List nodeList = new ArrayList<>(); + if (!isThereAnyChild()) { + if (!(getOnlyReal && !lodNodeData.dirty) + && !(getOnlyReal && !lodNodeData.real)) { + nodeList.add(lodNodeData); + } + } else { + if (!getOnlyLeaf + && !(getOnlyDirty && !lodNodeData.dirty) + && !(getOnlyReal && !lodNodeData.real)) { + nodeList.add(lodNodeData); + } + for (int NS = 0; NS <= 1; NS++) { + for (int WE = 0; WE <= 1; WE++) { + LodQuadTree child = children[NS][WE]; + if (child != null) { + nodeList.addAll(child.getNodeList(getOnlyReal, getOnlyDirty, getOnlyLeaf)); + } + } + } + } + return nodeList; + } + + /** + * method to get certain nodes from the LodQuadTree + * @return list of nodes + */ + public List getNodeToRender(int x, int z, byte targetLevel, int maxDistance, int minDistance){ + int distance = (int) Math.sqrt(Math.pow(x + lodNodeData.centerX,2) + Math.pow(z + lodNodeData.centerZ,2)); + List nodeList = new ArrayList<>(); + if(distance > maxDistance || distance < minDistance || targetLevel > lodNodeData.level) { + return nodeList; + } + if(targetLevel == lodNodeData.level || !isThereAnyChild()){ + if(!lodNodeData.voidNode){ + nodeList.add(lodNodeData); + return nodeList; + }else{ + return nodeList; + } + } else { + for (int NS = 0; NS <= 1; NS++) { + for (int WE = 0; WE <= 1; WE++) { + LodQuadTree child = children[NS][WE]; + if (child != null) { + nodeList.addAll(child.getNodeToRender(x,z,targetLevel,maxDistance,minDistance)); + } + } + } + } + return nodeList; + } + + /** + * method to get certain nodes from the LodQuadTree + * @return list of nodes + */ + public List> getNodeToGenerate(int x, int z, byte targetLevel, int maxDistance, int minDistance){ + int distance = (int) Math.sqrt(Math.pow(x + lodNodeData.centerX,2) + Math.pow(z + lodNodeData.centerZ,2)); + List> nodeList = new ArrayList<>(); + if(distance > maxDistance || distance < minDistance || targetLevel > lodNodeData.level) { + return nodeList; + } + if(targetLevel == lodNodeData.level){ + return nodeList; + } else { + if(!isThereAnyChild()){ + for (int NS = 0; NS <= 1; NS++) { + for (int WE = 0; WE <= 1; WE++) { + LodQuadTree child = children[NS][WE]; + if (child != null) { + nodeList.addAll(child.getNodeToGenerate(x,z,targetLevel,maxDistance,minDistance)); + } + } + } + }else{ + nodeList.add( new AbstractMap.SimpleEntry<>(lodNodeData,distance)); + } + } + return nodeList; + } + /** + * simple getter for lodNodeData + * + * @return lodNodeData + */ + public LodNodeData getLodNodeData() { + return lodNodeData; + } + + /** + * setter for lodNodeData, to maintain a correct relationship between level this method force update on all parent + * + * @param newLodNodeData data to set + * @param updateHigherLevel if true it will update all the upper levels. + */ + public void setLodNodeData(LodNodeData newLodNodeData, boolean updateHigherLevel) { + this.lodNodeData.update(lodNodeData); + //a recursive update is necessary to change higher level + if (parent != null && updateHigherLevel) parent.updateLevel(true); + } + + public boolean isNodeFull() { + return nodeFull; + } + + public boolean isThereAnyChild() { + return nodeEmpty; + } + + public boolean isNodeReal() { + return lodNodeData.real; + } + + public boolean isRenderable() { + return (lodNodeData != null); + } +} + +/* +EXAMPLES OF USES + + */ diff --git a/src/main/java/com/seibel/lod/objects/quadTree/LodQuadTreeDimension.java b/src/main/java/com/seibel/lod/objects/quadTree/LodQuadTreeDimension.java new file mode 100644 index 000000000..c864f6134 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/quadTree/LodQuadTreeDimension.java @@ -0,0 +1,418 @@ +package com.seibel.lod.objects.quadTree; + +import com.seibel.lod.handlers.LodQuadTreeDimensionFileHandler; +import com.seibel.lod.objects.LodWorld; +import com.seibel.lod.objects.RegionPos; +import com.seibel.lod.util.LodUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.world.DimensionType; +import net.minecraft.world.server.ServerChunkProvider; +import net.minecraft.world.server.ServerWorld; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class LodQuadTreeDimension { + public final DimensionType dimension; + + private volatile int width; + private volatile int halfWidth; + public long seed; + + public volatile LodQuadTree regions[][]; + public volatile boolean isRegionDirty[][]; + + private int centerX; + private int centerZ; + + private LodQuadTreeDimensionFileHandler fileHandler; + + + public LodQuadTreeDimension(DimensionType newDimension, LodWorld lodWorld, int newMaxWidth) + { + dimension = newDimension; + width = newMaxWidth; + + try + { + Minecraft mc = Minecraft.getInstance(); + + File saveDir; + if(mc.isIntegratedServerRunning()) + { + // local world + + ServerWorld serverWorld = LodUtil.getServerWorldFromDimension(newDimension); + seed = serverWorld.getSeed(); + // provider needs a separate variable to prevent + // the compiler from complaining + ServerChunkProvider provider = serverWorld.getChunkProvider(); + saveDir = new File(provider.getSavedData().folder.getCanonicalFile().getPath() + File.separatorChar + "lod"); + } + else + { + // connected to server + + saveDir = new File(mc.gameDir.getCanonicalFile().getPath() + + File.separatorChar + "lod server data" + File.separatorChar + LodUtil.getDimensionIDFromWorld(mc.world)); + } + + fileHandler = new LodQuadTreeDimensionFileHandler(saveDir, this); + } + catch(IOException e) + { + // the file handler wasn't able to be created + // we won't be able to read or write any files + } + + + regions = new LodQuadTree[width][width]; + isRegionDirty = new boolean[width][width]; + + // populate isRegionDirty + for(int i = 0; i < width; i++) + for(int j = 0; j < width; j++) + isRegionDirty[i][j] = false; + + centerX = 0; + centerZ = 0; + + halfWidth = (int)Math.floor(width / 2); + } + + + /** + * Move the center of this LodDimension and move all owned + * regions over by the given x and z offset. + */ + public void move(int xOffset, int zOffset) + { + // if the x or z offset is equal to or greater than + // the total size, just delete the current data + // and update the centerX and/or centerZ + if (Math.abs(xOffset) >= width || Math.abs(zOffset) >= width) + { + for(int x = 0; x < width; x++) + { + for(int z = 0; z < width; z++) + { + regions[x][z] = null; + } + } + + // update the new center + centerX += xOffset; + centerZ += zOffset; + + return; + } + + + // X + if(xOffset > 0) + { + // move everything over to the left (as the center moves to the right) + for(int x = 0; x < width; x++) + { + for(int z = 0; z < width; z++) + { + if(x + xOffset < width) + regions[x][z] = regions[x + xOffset][z]; + else + regions[x][z] = null; + } + } + } + else + { + // move everything over to the right (as the center moves to the left) + for(int x = width - 1; x >= 0; x--) + { + for(int z = 0; z < width; z++) + { + if(x + xOffset >= 0) + regions[x][z] = regions[x + xOffset][z]; + else + regions[x][z] = null; + } + } + } + + + + // Z + if(zOffset > 0) + { + // move everything up (as the center moves down) + for(int x = 0; x < width; x++) + { + for(int z = 0; z < width; z++) + { + if(z + zOffset < width) + regions[x][z] = regions[x][z + zOffset]; + else + regions[x][z] = null; + } + } + } + else + { + // move everything down (as the center moves up) + for(int x = 0; x < width; x++) + { + for(int z = width - 1; z >= 0; z--) + { + if(z + zOffset >= 0) + regions[x][z] = regions[x][z + zOffset]; + else + regions[x][z] = null; + } + } + } + + + + // update the new center + centerX += xOffset; + centerZ += zOffset; + } + + + + + + + /** + * Gets the region at the given X and Z + *
+ * Returns null if the region doesn't exist + * or is outside the loaded area. + */ + public LodQuadTree getRegion(int regionX, int regionZ) + { + int xIndex = (regionX - centerX) + halfWidth; + int zIndex = (regionZ - centerZ) + halfWidth; + + if (!regionIsInRange(regionX, regionZ)) + // out of range + return null; + + if (regions[xIndex][zIndex] == null) + { + regions[xIndex][zIndex] = getRegionFromFile(regionX, regionZ); + if (regions[xIndex][zIndex] == null) + { + regions[xIndex][zIndex] = new LodQuadTree(regionX, regionZ); + } + } + + return regions[xIndex][zIndex]; + } + + /** + * Overwrite the LodRegion at the location of newRegion with newRegion. + * @throws ArrayIndexOutOfBoundsException if newRegion is outside what can be stored in this LodDimension. + */ + public void setRegion(LodQuadTree newRegion) throws ArrayIndexOutOfBoundsException + { + int xIndex = (newRegion.getLodNodeData().posX - centerX) + halfWidth; + int zIndex = (centerZ - newRegion.getLodNodeData().posZ) + halfWidth; + + if (!regionIsInRange(newRegion.getLodNodeData().posX, newRegion.getLodNodeData().posZ)) + // out of range + throw new ArrayIndexOutOfBoundsException(); + + regions[xIndex][zIndex] = newRegion; + } + + + + + + /** + * Add the given LOD to this dimension at the coordinate + * stored in the LOD. If an LOD already exists at the given + * coordinates it will be overwritten. + */ + public void addNode(LodNodeData lodNodeData) + { + RegionPos pos = new RegionPos( + lodNodeData.posX / lodNodeData.width, + lodNodeData.posZ / lodNodeData.width + ); + + // don't continue if the region can't be saved + if (!regionIsInRange(pos.x, pos.z)) + { + return; + } + + LodQuadTree region = getRegion(pos.x, pos.z); + + if (region == null) + { + // if no region exists, create it + region = new LodQuadTree(pos.x, pos.z); + setRegion(region); + } + + region.setNodeAtLowerLevel(lodNodeData, true); + + // don't save empty place holders to disk + if (!lodNodeData.real && fileHandler != null) + { + // mark the region as dirty so it will be saved to disk + int xIndex = (pos.x - centerX) + halfWidth; + int zIndex = (pos.z - centerZ) + halfWidth; + isRegionDirty[xIndex][zIndex] = true; + fileHandler.saveDirtyRegionsToFileAsync(); + } + } + + /** + * Get the LodNodeData at the given X and Z position in the level + * in this dimension. + *
+ * Returns null if the LodChunk doesn't exist or + * is outside the loaded area. + */ + public LodNodeData getLodFromCoordinates(int posX, int posZ, byte level) + { + /*TODO */ + return null; + /* + RegionPos pos = LodUtil.convertChunkPosToRegionPos(new ChunkPos(chunkX, chunkZ)); + + LodQuadTree region = getRegion(pos.x, pos.z); + + if(region == null) + return null; + + return region.getNode(chunkX, chunkZ); + + */ + } + + /** + * method to get all the nodes that have to be rendered based on the position of the player + * @return list of nodes + */ + public List getNodeToRender(int x, int z){ + //BASIC IDEA + //Get all the node to render from every region + //combine them toghether + return null; + } + + /** + * method to get all the nodes that have to be generated based on the position of the player + * @return list of nodes + */ + public List getNodeToGenerate(int x, int z){ + //BASIC IDEA + //Get all the node to generate from every region + //combine them toghether + //sort them based on the distance to player + //this way + return null; + } + + /** + * Get the region at the given X and Z coordinates from the + * RegionFileHandler. + */ + public LodQuadTree getRegionFromFile(int regionX, int regionZ) + { + if (fileHandler != null) + return fileHandler.loadRegionFromFile(regionX, regionZ); + else + return null; + } + + + /** + * Returns whether the region at the given X and Z coordinates + * is within the loaded range. + */ + public boolean regionIsInRange(int regionX, int regionZ) + { + int xIndex = (regionX - centerX) + halfWidth; + int zIndex = (regionZ - centerZ) + halfWidth; + + return xIndex >= 0 && xIndex < width && zIndex >= 0 && zIndex < width; + } + + + + + + + + public int getCenterX() + { + return centerX; + } + + public int getCenterZ() + { + return centerZ; + } + + + /** + * Returns how many non-null LodChunks + * are stored in this LodDimension. + */ + public int getNumberOfLods() + { + int numbLods = 0; + for (LodQuadTree[] regions : regions) + { + if(regions == null) + continue; + + for (LodQuadTree region : regions) + { + if(region == null) + continue; + + numbLods= region.getNodeList(false,false,true).size(); + } + } + + return numbLods; + } + + + public int getWidth() + { + return width; + } + + public void setRegionWidth(int newWidth) + { + width = newWidth; + halfWidth = (int)Math.floor(width / 2); + + regions = new LodQuadTree[width][width]; + isRegionDirty = new boolean[width][width]; + + // populate isRegionDirty + for(int i = 0; i < width; i++) + for(int j = 0; j < width; j++) + isRegionDirty[i][j] = false; + } + + + @Override + public String toString() + { + String s = ""; + + s += "dim: " + dimension.toString() + "\t"; + s += "(" + centerX + "," + centerZ + ")"; + + return s; + } +} diff --git a/src/main/java/com/seibel/lod/objects/quadTree/LodQuadTreeWorld.java b/src/main/java/com/seibel/lod/objects/quadTree/LodQuadTreeWorld.java new file mode 100644 index 000000000..199d7f751 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/quadTree/LodQuadTreeWorld.java @@ -0,0 +1,97 @@ +package com.seibel.lod.objects.quadTree; + +import net.minecraft.world.DimensionType; + +import java.util.Hashtable; +import java.util.Map; + +public class LodQuadTreeWorld { + + private String worldName; + + private Map lodDimensions; + /** + * If true then the LOD world is setup and ready to use + */ + private boolean isWorldLoaded = false; + + public static final String NO_WORLD_LOADED = "No world loaded"; + + public LodQuadTreeWorld() { + worldName = NO_WORLD_LOADED; + } + + /** + * Set up the LodWorld with the given newWorldName.
+ * This should be done whenever loading a new world. + * @param newWorldName name of the world + */ + public void selectWorld(String newWorldName) { + if (newWorldName.isEmpty()) { + deselectWorld(); + return; + } + + if (worldName.equals(newWorldName)) + // don't recreate everything if we + // didn't actually change worlds + return; + + worldName = newWorldName; + lodDimensions = new Hashtable<>(); + isWorldLoaded = true; + } + + /** + * Set the worldName to "No world loaded" + * and clear the lodDimensions Map.
+ * This should be done whenever unloaded a world. + */ + public void deselectWorld() { + worldName = NO_WORLD_LOADED; + lodDimensions = null; + isWorldLoaded = false; + } + + + public void addLodDimension(LodQuadTreeDimension newStorage) { + if (lodDimensions == null) + throw new IllegalStateException("LodWorld hasn't been given a world yet."); + + lodDimensions.put(newStorage.dimension, newStorage); + } + + public LodQuadTreeDimension getLodDimension(DimensionType dimension) { + if (lodDimensions.get(dimension) == null) { + throw new IllegalStateException("LodWorld hasn't been given a world yet."); + } + return lodDimensions.get(dimension); + } + + /** + * Resizes the max width in regions that each LodDimension + * should use. + */ + public void resizeDimensionRegionWidth(int newWidth) { + if (lodDimensions == null) + throw new IllegalStateException("LodWorld hasn't been given a world yet."); + + for (DimensionType key : lodDimensions.keySet()) + lodDimensions.get(key).setRegionWidth(newWidth); + } + + + public boolean getIsWorldLoaded() { + return isWorldLoaded; + } + + public String getWorldName() { + return worldName; + } + + @Override + public String toString() { + return "World name: " + worldName; + } +} + diff --git a/src/main/java/com/seibel/lod/objects/quadTree/UsesExamples.java b/src/main/java/com/seibel/lod/objects/quadTree/UsesExamples.java new file mode 100644 index 000000000..338b27035 --- /dev/null +++ b/src/main/java/com/seibel/lod/objects/quadTree/UsesExamples.java @@ -0,0 +1,58 @@ +package com.seibel.lod.objects.quadTree; + +import com.seibel.lod.builders.LodBuilder; +import com.seibel.lod.builders.LodNodeBuilder; +import com.seibel.lod.objects.LodDimension; +import net.minecraft.client.Minecraft; +import net.minecraft.world.DimensionType; + +import java.awt.*; +import java.lang.reflect.Array; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.Map; + +public class UsesExamples { + public static void main(String[] args){ + //THIS CODE DOESN'T WORK AT THE MOMENT + /**TODO + * Complete all the new Lod objects + * Complete the getNodeToGenerate in LodQuadTreeDimension + * Complete the getNodeToRender in LodQuadTreeDimension + * Complete the node builder + * Complete the new renderer + * add everything to ClientProxy for the first test + * */ + + LodQuadTreeWorld lodWorld = new LodQuadTreeWorld(); + LodQuadTreeDimension newLodDimension = new LodQuadTreeDimension(Minecraft.getInstance().world.getDimensionType(), Minecraft.getInstance().world, 10); + lodWorld.addLodDimension(newLodDimension); + + LodQuadTreeDimension lodDimension = lodWorld.getLodDimension(Minecraft.getInstance().world.getDimensionType()); + lodDimension.move(0,0); + /* + I will now generate some fake LodNodeData. This in the final implementation will be generated by a builder + this lodNodeData will be put at level 6 of the QuadTree. This LodNodeData represent a block of width + */ + LodNodeData lodNodeData1 = new LodNodeData((byte) 6, 4, 4, 64, 0, new Color(0,200,0),true); + LodNodeData lodNodeData2 = new LodNodeData((byte) 6, 4, 5, 64, 0, new Color(0,200,0),true); + LodNodeData lodNodeData3 = new LodNodeData((byte) 6, 5, 4, 64, 0, new Color(0,0,200),true); + /* + add like this + */ + lodDimension.addNode(lodNodeData1); + lodDimension.addNode(lodNodeData2); + lodDimension.addNode(lodNodeData3); + /* + Automatic generation + + */ + List nodeToGenerate = (List) lodDimension.getRegion(0,0).getNodeToGenerate(0,0, (byte) 3,1000,0); + Collections.sort(nodeToGenerate, Map.Entry.comparingByValue(); + + /* + Call the builder to generate all the useful node + */ + + } +}