diff --git a/src/main/java/com/seibel/lod/core/enums/config/DistanceGenerationMode.java b/src/main/java/com/seibel/lod/core/enums/config/DistanceGenerationMode.java index 712de3cc3..20e2b93e8 100644 --- a/src/main/java/com/seibel/lod/core/enums/config/DistanceGenerationMode.java +++ b/src/main/java/com/seibel/lod/core/enums/config/DistanceGenerationMode.java @@ -19,6 +19,8 @@ package com.seibel.lod.core.enums.config; +import org.jetbrains.annotations.Nullable; + /** * NONE
* BIOME_ONLY
@@ -92,4 +94,46 @@ public enum DistanceGenerationMode { this.complexity = complexity; } + + // Note: return null if out of range + @Nullable + public static DistanceGenerationMode previous(DistanceGenerationMode mode) { + switch (mode) { + case FULL: + return DistanceGenerationMode.FEATURES; + case FEATURES: + return DistanceGenerationMode.SURFACE; + case SURFACE: + return DistanceGenerationMode.BIOME_ONLY_SIMULATE_HEIGHT; + case BIOME_ONLY_SIMULATE_HEIGHT: + return DistanceGenerationMode.BIOME_ONLY; + case BIOME_ONLY: + return DistanceGenerationMode.NONE; + case NONE: + return null; + default: + return null; + } + } + + // Note: return null if out of range + @Nullable + public static DistanceGenerationMode next(DistanceGenerationMode mode) { + switch (mode) { + case FULL: + return null; + case FEATURES: + return DistanceGenerationMode.FULL; + case SURFACE: + return DistanceGenerationMode.FEATURES; + case BIOME_ONLY_SIMULATE_HEIGHT: + return DistanceGenerationMode.SURFACE; + case BIOME_ONLY: + return DistanceGenerationMode.BIOME_ONLY_SIMULATE_HEIGHT; + case NONE: + return DistanceGenerationMode.BIOME_ONLY; + default: + return null; + } + } } diff --git a/src/main/java/com/seibel/lod/core/enums/config/VerticalQuality.java b/src/main/java/com/seibel/lod/core/enums/config/VerticalQuality.java index 9d82f9486..494f78620 100644 --- a/src/main/java/com/seibel/lod/core/enums/config/VerticalQuality.java +++ b/src/main/java/com/seibel/lod/core/enums/config/VerticalQuality.java @@ -19,6 +19,8 @@ package com.seibel.lod.core.enums.config; +import org.jetbrains.annotations.Nullable; + /** * heightmap
* multi_lod
@@ -77,4 +79,34 @@ public enum VerticalQuality { this.maxVerticalData = maxVerticalData; } + + // Note: return null if out of range + @Nullable + public static VerticalQuality previous(VerticalQuality mode) { + switch (mode) { + case HIGH: + return VerticalQuality.MEDIUM; + case MEDIUM: + return VerticalQuality.LOW; + case LOW: + return null; + default: + return null; + } + } + + // Note: return null if out of range + @Nullable + public static VerticalQuality next(VerticalQuality mode) { + switch (mode) { + case HIGH: + return null; + case MEDIUM: + return VerticalQuality.HIGH; + case LOW: + return VerticalQuality.MEDIUM; + default: + return null; + } + } } \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java index 3c1362425..0f0297906 100644 --- a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java +++ b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java @@ -29,6 +29,7 @@ import java.util.concurrent.Executors; import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; +import org.jetbrains.annotations.Nullable; import com.seibel.lod.core.api.ClientApi; import com.seibel.lod.core.enums.config.DistanceGenerationMode; @@ -100,6 +101,8 @@ public class LodDimensionFileHandler + + //================// // read from file // //================// @@ -116,125 +119,65 @@ public class LodDimensionFileHandler for (byte tempDetailLevel = LodUtil.REGION_DETAIL_LEVEL; tempDetailLevel >= detailLevel; tempDetailLevel--) { - String fileName = getFileNameAndPathForRegion(regionX, regionZ, generationMode, tempDetailLevel, verticalQuality); + File file = getBestMatchingRegionFile(tempDetailLevel, regionX, regionZ, generationMode, verticalQuality); + if (file == null) continue; // Failed to find the file for this detail level. continue and try next one - try + long fileSize = file.length(); + if (fileSize == 0) continue; // file is empty. Let's not try parsing empty files + try (XZCompressorInputStream inputStream = new XZCompressorInputStream(new FileInputStream(file))) { - // if the fileName was null that means the folder is inaccessible - // for some reason - if (fileName == null) - throw new IllegalArgumentException("Unable to read region [" + regionX + ", " + regionZ + "] file, no fileName."); + int fileVersion; + fileVersion = inputStream.read(); - File file = new File(fileName); - if (!file.exists()) + // check if this file can be read by this file handler + if (fileVersion < 6) { - //there is no file for current gen mode - //search others above current from the most to the least detailed - VerticalQuality tempVerticalQuality = VerticalQuality.HIGH; - do { - DistanceGenerationMode tempGenMode = DistanceGenerationMode.FULL; - do { - fileName = getFileNameAndPathForRegion(regionX, regionZ, tempGenMode, tempDetailLevel, verticalQuality); - if (fileName != null) - { - file = new File(fileName); - if (file.exists()) - break; - } - //decrease gen mode - if (tempGenMode == DistanceGenerationMode.FULL) - tempGenMode = DistanceGenerationMode.FEATURES; - else if (tempGenMode == DistanceGenerationMode.FEATURES) - tempGenMode = DistanceGenerationMode.SURFACE; - else if (tempGenMode == DistanceGenerationMode.SURFACE) - tempGenMode = DistanceGenerationMode.BIOME_ONLY_SIMULATE_HEIGHT; - else if (tempGenMode == DistanceGenerationMode.BIOME_ONLY_SIMULATE_HEIGHT) - tempGenMode = DistanceGenerationMode.BIOME_ONLY; - else if (tempGenMode == DistanceGenerationMode.BIOME_ONLY) - tempGenMode = DistanceGenerationMode.NONE; - } while (tempGenMode != generationMode); - if (fileName != null) - { - file = new File(fileName); - if (file.exists()) - break; - } - if (tempVerticalQuality == VerticalQuality.HIGH) - tempVerticalQuality = VerticalQuality.MEDIUM; - else if (tempVerticalQuality == VerticalQuality.MEDIUM) - tempVerticalQuality = VerticalQuality.LOW; - } while (tempVerticalQuality != verticalQuality); - if (!file.exists()) - //there wasn't a file, don't return anything - continue; + // the file we are reading is an older version, + // close the reader and delete the file. + inputStream.close(); + file.delete(); + ClientApi.LOGGER.info("Outdated LOD region file for region: (" + regionX + "," + regionZ + ")" + + " version found: " + fileVersion + + ", version requested: " + LOD_SAVE_FILE_VERSION + + ". File has been deleted."); + // This should not break, but be continue to see whether other detail levels can be loaded or updated + continue; } - - - - // don't try parsing empty files - long dataSize = file.length(); - dataSize -= 1; - if (dataSize > 0) + else if (fileVersion > LOD_SAVE_FILE_VERSION) { - try (XZCompressorInputStream inputStream = new XZCompressorInputStream(new FileInputStream(file))) - { - int fileVersion; - fileVersion = inputStream.read(); - - // check if this file can be read by this file handler - if (fileVersion < 6) - { - // the file we are reading is an older version, - // close the reader and delete the file. - inputStream.close(); - file.delete(); - ClientApi.LOGGER.info("Outdated LOD region file for region: (" + regionX + "," + regionZ + ")" - + " version found: " + fileVersion - + ", version requested: " + LOD_SAVE_FILE_VERSION - + ". File was been deleted."); - // This should not break, but be continue to see whether other versions can be loaded or updated - continue; - } - 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 accidentally delete anything the user may want. - inputStream.close(); - ClientApi.LOGGER.info("Newer LOD region file for region: (" + regionX + "," + regionZ + ")" - + " version found: " + fileVersion - + ", version requested: " + LOD_SAVE_FILE_VERSION - + " this region will not be written to in order to protect the newer file."); - // This should not break, but be continue to see whether other versions can be loaded or updated - continue; - } - else if (fileVersion < LOD_SAVE_FILE_VERSION) - { - // this is old, but readable version - // read and add the data to our region - region.addLevelContainer(new VerticalLevelContainer(new DataInputStream(inputStream), fileVersion)); - inputStream.close(); - } else - { - // this file is a readable version, - // read and add the data to our region - region.addLevelContainer(new VerticalLevelContainer(new DataInputStream(inputStream), LOD_SAVE_FILE_VERSION)); - inputStream.close(); - } - } - catch (IOException ioEx) - { - ClientApi.LOGGER.error("LOD file read error. Unable to read to [" + fileName + "] error [" + ioEx.getMessage() + "]: "); - ioEx.printStackTrace(); - } + // the file we are reading is a newer version, + // close the reader and ignore the file, we don't + // want to accidentally delete anything the user may want. + inputStream.close(); + ClientApi.LOGGER.info("Newer LOD region file for region: (" + regionX + "," + regionZ + ")" + + " version found: " + fileVersion + + ", version requested: " + LOD_SAVE_FILE_VERSION + + " this region will not be written to in order to protect the newer file."); + // This should not break, but be continue to see whether other detail levels can be loaded or updated + continue; + } + else if (fileVersion < LOD_SAVE_FILE_VERSION) + { + ClientApi.LOGGER.debug("Old LOD region file for region: (" + regionX + "," + regionZ + ")" + + " version found: " + fileVersion + + ", version requested: " + LOD_SAVE_FILE_VERSION + + ". File will be loaded and updated to new format in next save."); + // this is old, but readable version + // read and add the data to our region + region.addLevelContainer(new VerticalLevelContainer(new DataInputStream(inputStream), fileVersion)); + inputStream.close(); + } else + { + // this file is a readable version, + // read and add the data to our region + region.addLevelContainer(new VerticalLevelContainer(new DataInputStream(inputStream), LOD_SAVE_FILE_VERSION)); + inputStream.close(); } } - catch (Exception e) + catch (IOException ioEx) { - // the buffered reader encountered a - // problem reading the file - ClientApi.LOGGER.error("LOD file read error. Unable to read to [" + fileName + "] error [" + e.getMessage() + "]: "); - e.printStackTrace(); + ClientApi.LOGGER.error("LOD file read error. Unable to read xz compressed file [" + file + "] error [" + ioEx.getMessage() + "]: "); + ioEx.printStackTrace(); } }// for each detail level @@ -289,97 +232,102 @@ public class LodDimensionFileHandler { for (byte detailLevel = region.getMinDetailLevel(); detailLevel <= LodUtil.REGION_DETAIL_LEVEL; detailLevel++) { - String fileName = getFileNameAndPathForRegion(region.regionPosX, region.regionPosZ, region.getGenerationMode(), detailLevel, region.getVerticalQuality()); + // Get the old file + File oldFile = getRegionFile(region.regionPosX, region.regionPosZ, region.getGenerationMode(), detailLevel, region.getVerticalQuality()); + ClientApi.LOGGER.debug("saving region [" + region.regionPosX + ", " + region.regionPosZ + "] to file."); - // if the fileName was null that means the folder is inaccessible - // for some reason - if (fileName == null) + boolean isFileFullyGened = false; + // make sure the file and folder exists + if (!oldFile.exists()) { - ClientApi.LOGGER.warn("Unable to save region [" + region.regionPosX + ", " + region.regionPosZ + "] to file, file is inaccessible."); - return; - } - File oldFile = new File(fileName); - //ClientProxy.LOGGER.info("saving region [" + region.regionPosX + ", " + region.regionPosZ + "] to file."); - //byte[] temp = region.getLevel(detailLevel).toDataString(); - - try - { - boolean isFileFullyGened = false; - // make sure the file and folder exists - if (!oldFile.exists()) - { - // the file doesn't exist, - // create it and the folder if need be - if (!oldFile.getParentFile().exists()) - oldFile.getParentFile().mkdirs(); + // the file doesn't exist, + // create it and the folder if need be + if (!oldFile.getParentFile().exists()) + oldFile.getParentFile().mkdirs(); + try { oldFile.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) - int fileVersion = LOD_SAVE_FILE_VERSION; - try (XZCompressorInputStream inputStream = new XZCompressorInputStream(new FileInputStream(oldFile))) - { - fileVersion = inputStream.read(); - inputStream.skip(1); - isFileFullyGened = (inputStream.read() & 0b10000000) != 0; - inputStream.close(); - } - catch (IOException ex) - { - ex.printStackTrace(); - } - - // check if this file can be written to by the file handler - if (fileVersion > LOD_SAVE_FILE_VERSION) - { - // the file we are reading is a newer version, - // don't write anything, we don't want to accidentally - // delete anything the user may want. - continue; - } - // if we got this far then we are good - // to overwrite the old file - } - // the old file is good, now create a new temporary save file - File newFile = new File(fileName + TMP_FILE_EXTENSION); - try (XZCompressorOutputStream outputStream = new XZCompressorOutputStream(new FileOutputStream(newFile), 3)) - { - // add the version of this file - outputStream.write(LOD_SAVE_FILE_VERSION); - - // add each LodChunk to the file - boolean isLocalFullyGened = region.getLevel(detailLevel).writeData(new DataOutputStream(outputStream)); - outputStream.close(); - - if (!isLocalFullyGened && isFileFullyGened) - { - // existing file is complete while new one is only partially generate - // this can happen is for some reason loading failed - // this doesn't fix the bug, but at least protects old data - ClientApi.LOGGER.error("LOD file write error. Attempted to overwrite complete region with incomplete one [" + fileName + "]"); - try { - newFile.delete(); - } catch (SecurityException e) { - // Failed to delete temp file... just continue. - } - continue; - } - - // overwrite the old file with the new one - Files.move(newFile.toPath(), oldFile.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } - catch (IOException ex) - { - ex.printStackTrace(); + } catch (IOException e) { + ClientApi.LOGGER.error("LOD file write error. Unable to create parent directory for [" + oldFile + "] error [" + e.getMessage() + "]: "); + e.printStackTrace(); + continue; } } - catch (Exception e) + else { - ClientApi.LOGGER.error("LOD file write error. Unable to write to [" + fileName + "] error [" + e.getMessage() + "]: "); + // the file exists, make sure it + // is the correct version. + // (to make sure we don't overwrite a newer + // version file if it exists) + int fileVersion = LOD_SAVE_FILE_VERSION; + try (XZCompressorInputStream inputStream = new XZCompressorInputStream(new FileInputStream(oldFile))) + { + fileVersion = inputStream.read(); + inputStream.skip(1); + isFileFullyGened = (inputStream.read() & 0b10000000) != 0; + inputStream.close(); + } + catch (IOException e) + { + ClientApi.LOGGER.warn("LOD file write warning. Unable to read existing file [" + oldFile + "] version. Treating it as latest version. [" + e.getMessage() + "]: "); + e.printStackTrace(); + } + + // check if this file can be written to by the file handler + if (fileVersion > LOD_SAVE_FILE_VERSION) + { + // the file we are reading is a newer version, + // don't write anything, we don't want to accidentally + // delete anything the user may want. + continue; + } + // if we got this far then we are good + // to overwrite the old file + } + + // Now create a new temporary save file + File tempFile; + try { + tempFile = File.createTempFile(oldFile.getName(), TMP_FILE_EXTENSION); + } catch (IOException e) { + ClientApi.LOGGER.error("LOD file write error. Unable to create temp file for [" + oldFile + "] error [" + e.getMessage() + "]: "); + e.printStackTrace(); + continue; + } + tempFile.deleteOnExit(); // Mark it to be deleted on exit if any unexcepted terminations happen + try (XZCompressorOutputStream outputStream = new XZCompressorOutputStream(new FileOutputStream(tempFile), 3)) + { + // add the version of this file + outputStream.write(LOD_SAVE_FILE_VERSION); + // add each LodChunk to the file + boolean isNewDataFullyGened = region.getLevel(detailLevel).writeData(new DataOutputStream(outputStream)); + outputStream.close(); + + if (!isNewDataFullyGened && isFileFullyGened) + { + // existing file is complete while new one is only partially generate + // this can happen is for some reason loading failed + // this doesn't fix the bug, but at least protects old data + ClientApi.LOGGER.error("LOD file write error. Attempted to overwrite complete region with incomplete one [" + oldFile + "]"); + try { + tempFile.delete(); + } catch (SecurityException e) { + // Failed to delete temp file... just continue. + } + continue; + } + } + catch (IOException e) + { + ClientApi.LOGGER.error("LOD file write error. Unable to write to temp file [" + tempFile + "] error [" + e.getMessage() + "]: "); + e.printStackTrace(); + continue; + } + + // overwrite the old file with the new one + try { + Files.move(tempFile.toPath(), oldFile.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + ClientApi.LOGGER.error("LOD file write error. Unable to update file [" + oldFile + "] error [" + e.getMessage() + "]: "); e.printStackTrace(); } } @@ -398,27 +346,40 @@ public class LodDimensionFileHandler *

* Returns null if there is an IO or security Exception. */ - private String getFileNameAndPathForRegion(int regionX, int regionZ, DistanceGenerationMode generationMode, byte detailLevel, VerticalQuality verticalQuality) - { - try - { - // saveFolder is something like - // ".\Super Flat\DIM-1\data\" - // or - // ".\Super Flat\data\" - return dimensionDataSaveFolder.getCanonicalPath() + File.separatorChar + - verticalQuality + File.separatorChar + - generationMode.toString() + File.separatorChar + - DETAIL_FOLDER_NAME_PREFIX + detailLevel + File.separatorChar + - FILE_NAME_PREFIX + "." + regionX + "." + regionZ + FILE_EXTENSION; - } - catch (IOException | SecurityException e) - { - ClientApi.LOGGER.warn("Unable to get the filename for the region [" + regionX + ", " + regionZ + "], error: [" + e.getMessage() + "].\n" - + "One possible cause is that the process failed to read the current path location due to security config. Stacktrace: "); - e.printStackTrace(); - return null; + + private String getFileBasePath() { + try { + return dimensionDataSaveFolder.getCanonicalPath() + File.separatorChar; + } catch (IOException e) { + ClientApi.LOGGER.warn("Unable to get the base save file path. One possible cause is that" + + " the process failed to read the current path location due to security configs."); + throw new RuntimeException("DistantHorizons Get Save File Path Failure"); } } + private File getRegionFile(int regionX, int regionZ, DistanceGenerationMode genMode, byte detail, VerticalQuality vertQuality) { + return new File(getFileBasePath() + vertQuality + File.separatorChar + + genMode + File.separatorChar + + DETAIL_FOLDER_NAME_PREFIX + detail + File.separatorChar + + FILE_NAME_PREFIX + "." + regionX + "." + regionZ + FILE_EXTENSION); + } + + // Return null if no file found + @Nullable + private File getBestMatchingRegionFile(byte detailLevel, int regionX, int regionZ, DistanceGenerationMode targetGenMode, VerticalQuality targetVertQuality) { + DistanceGenerationMode genMode = targetGenMode; + // Search from least GenMode to max GenMode, than least vertQuality to max vertQuality + do { + File file = getRegionFile(regionX, regionZ, genMode, detailLevel, targetVertQuality); + if (file.exists()) return file; // Found target file. + targetGenMode = DistanceGenerationMode.next(targetGenMode); + if (targetGenMode == null) { // Failed to find any files for this vertQuality. Try next one up. + targetGenMode = genMode; + targetVertQuality = VerticalQuality.next(targetVertQuality); + } + } while (targetVertQuality != null); + return null; + } + + } diff --git a/src/main/java/com/seibel/lod/core/util/ThreadMapUtil.java b/src/main/java/com/seibel/lod/core/util/ThreadMapUtil.java index 9c2012f6c..5cf02a501 100644 --- a/src/main/java/com/seibel/lod/core/util/ThreadMapUtil.java +++ b/src/main/java/com/seibel/lod/core/util/ThreadMapUtil.java @@ -28,6 +28,8 @@ import java.util.concurrent.ConcurrentMap; import com.seibel.lod.core.enums.LodDirection; import com.seibel.lod.core.objects.VertexOptimizer; +// FIXME: Nuke this whole thing and use ThreadLocal instead. And no more redundant get() please! + /** * Holds data used by specific threads so * the data doesn't have to be recreated every @@ -116,7 +118,8 @@ public class ThreadMapUtil //________________________// - + + //TODO: Maybe use actual valid total world height instead of always assuming the worse and alloc 1024 blocks. /** returns the array filled with 0's */ public static long[] getBuilderVerticalArray(int detailLevel) { @@ -138,19 +141,19 @@ public class ThreadMapUtil /** returns the array filled with 0's */ public static long[] getVerticalDataArray(int arrayLength) { - if (!threadVerticalAddDataMap.containsKey(Thread.currentThread().getName()) || (threadVerticalAddDataMap.get(Thread.currentThread().getName()) == null)) + long[] array = threadVerticalAddDataMap.get(Thread.currentThread().getName()); + if (array == null || array.length != arrayLength) { - threadVerticalAddDataMap.put(Thread.currentThread().getName(), new long[arrayLength]); + array = new long[arrayLength]; + threadVerticalAddDataMap.put(Thread.currentThread().getName(), array); } else - { - Arrays.fill(threadVerticalAddDataMap.get(Thread.currentThread().getName()), 0); - } - return threadVerticalAddDataMap.get(Thread.currentThread().getName()); + Arrays.fill(array, 0); + return array; } - - + + //FIXME: If the arrayLength change, this may return incorrect sized array /** returns the array NOT cleared every time */ public static short[] getHeightAndDepth(int arrayLength) { @@ -165,18 +168,19 @@ public class ThreadMapUtil /** returns the array filled with 0's */ public static long[] getVerticalUpdateArray(int detailLevel) { - if (!verticalUpdate.containsKey(Thread.currentThread().getName()) || (verticalUpdate.get(Thread.currentThread().getName()) == null)) + long[][] arrays = verticalUpdate.get(Thread.currentThread().getName()); + if (arrays == null) { - long[][] array = new long[LodUtil.DETAIL_OPTIONS][]; - for (int i = 1; i < LodUtil.DETAIL_OPTIONS; i++) - array[i] = new long[DetailDistanceUtil.getMaxVerticalData(i - 1) * 4]; - verticalUpdate.put(Thread.currentThread().getName(), array); + arrays = new long[LodUtil.DETAIL_OPTIONS][]; + verticalUpdate.put(Thread.currentThread().getName(), arrays); } + long[] array = arrays[detailLevel]; + int arrayLength = DetailDistanceUtil.getMaxVerticalData(detailLevel) * 4; + if (array == null || array.length != arrayLength) + array = new long[arrayLength]; else - { - Arrays.fill(verticalUpdate.get(Thread.currentThread().getName())[detailLevel], 0); - } - return verticalUpdate.get(Thread.currentThread().getName())[detailLevel]; + Arrays.fill(array, 0); + return array; } /** clears all arrays so they will have to be rebuilt */