/* * This file is part of the Distant Horizon mod (formerly the LOD Mod), * licensed under the GNU GPL v3 License. * * Copyright (C) 2020 James Seibel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.seibel.lod.handlers; import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; import com.seibel.lod.enums.DistanceGenerationMode; import com.seibel.lod.enums.VerticalQuality; import com.seibel.lod.objects.LodDimension; import com.seibel.lod.objects.LodRegion; import com.seibel.lod.objects.RegionPos; import com.seibel.lod.objects.VerticalLevelContainer; import com.seibel.lod.proxy.ClientProxy; import com.seibel.lod.util.LodThreadFactory; import com.seibel.lod.util.LodUtil; import com.seibel.lod.util.ThreadMapUtil; /** * This object handles creating LodRegions * from files and saving LodRegion objects * to file. * * @author James Seibel * @author Cola * @version 9-25-2021 */ public class LodDimensionFileHandler { /** This is the dimension that owns this file handler */ private LodDimension lodDimension; private final File dimensionDataSaveFolder; /** lod */ private static final String FILE_NAME_PREFIX = "lod"; /** .txt */ private static final String FILE_EXTENSION = ".xz"; /** detail- */ private static final String DETAIL_FOLDER_NAME_PREFIX = "detail-"; /** * .tmp
* Added to the end of the file path when saving to prevent * nulling a currently existing file.
* After the file finishes saving it will end with * FILE_EXTENSION. */ private static final String TMP_FILE_EXTENSION = ".tmp"; /** * 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 = 6; /** * Allow saving asynchronously, but never try to save multiple regions * at a time */ private final ExecutorService fileWritingThreadPool = Executors.newSingleThreadExecutor(new LodThreadFactory(this.getClass().getSimpleName())); public LodDimensionFileHandler(File newSaveFolder, LodDimension newLodDimension) { if (newSaveFolder == null) throw new IllegalArgumentException("LodDimensionFileHandler requires a valid File location to read and write to."); dimensionDataSaveFolder = newSaveFolder; lodDimension = newLodDimension; } //================// // read from file // //================// /** * Returns the LodRegion at the given coordinates. * Returns an empty region if the file doesn't exist. */ public LodRegion loadRegionFromFile(byte detailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) { int regionX = regionPos.x; int regionZ = regionPos.z; LodRegion region = new LodRegion(LodUtil.REGION_DETAIL_LEVEL, regionPos, generationMode, verticalQuality); for (byte tempDetailLevel = LodUtil.REGION_DETAIL_LEVEL; tempDetailLevel >= detailLevel; tempDetailLevel--) { String fileName = getFileNameAndPathForRegion(regionX, regionZ, generationMode, tempDetailLevel, verticalQuality); try { // 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."); File file = new File(fileName); if (!file.exists()) { //there is no file for current gen mode //search others above current from the most to the least detailed DistanceGenerationMode tempGenMode = DistanceGenerationMode.SERVER; while (tempGenMode != generationMode) { fileName = getFileNameAndPathForRegion(regionX, regionZ, tempGenMode, tempDetailLevel, verticalQuality); if (fileName != null) { file = new File(fileName); if (file.exists()) break; } //decrease gen mode if (tempGenMode == DistanceGenerationMode.SERVER) 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; } if (!file.exists()) //there wasn't a file, don't return anything continue; } // don't try parsing empty files long dataSize = file.length(); dataSize -= 1; if (dataSize > 0) { 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 < LOD_SAVE_FILE_VERSION) { // the file we are reading is an older version, // close the reader and delete the file. inputStream.close(); file.delete(); ClientProxy.LOGGER.info("Outdated LOD region file for region: (" + regionX + "," + regionZ + ")" + " version found: " + fileVersion + ", version requested: " + LOD_SAVE_FILE_VERSION + ". File was been deleted."); break; } 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(); ClientProxy.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."); break; } // this file is a readable version, // read the file byte[] data = ThreadMapUtil.getSaveContainer(tempDetailLevel); inputStream.read(data); inputStream.close(); // add the data to our region region.addLevelContainer(new VerticalLevelContainer(data)); } catch (IOException ioEx) { ClientProxy.LOGGER.error("LOD file read error. Unable to read to [" + fileName + "] error [" + ioEx.getMessage() + "]: "); ioEx.printStackTrace(); } } } catch (Exception e) { // the buffered reader encountered a // problem reading the file ClientProxy.LOGGER.error("LOD file read error. Unable to read to [" + fileName + "] error [" + e.getMessage() + "]: "); e.printStackTrace(); } }// for each detail level if (region.getMinDetailLevel() >= detailLevel) region.growTree(detailLevel); return region; } //==============// // Save to File // //==============// /** Save all dirty regions in this LodDimension to file */ public void saveDirtyRegionsToFileAsync() { fileWritingThreadPool.execute(saveDirtyRegionsThread); } private final Thread saveDirtyRegionsThread = new Thread(() -> { try { for (int i = 0; i < lodDimension.getWidth(); i++) { for (int j = 0; j < lodDimension.getWidth(); j++) { if (lodDimension.GetIsRegionDirty(i, j) && lodDimension.getRegionByArrayIndex(i, j) != null) { saveRegionToFile(lodDimension.getRegionByArrayIndex(i, j)); lodDimension.SetIsRegionDirty(i, j, false); } } } } catch (Exception e) { e.printStackTrace(); } }); /** * 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 saveRegionToFile(LodRegion region) { for (byte detailLevel = region.getMinDetailLevel(); detailLevel <= LodUtil.REGION_DETAIL_LEVEL; detailLevel++) { String fileName = getFileNameAndPathForRegion(region.regionPosX, region.regionPosZ, region.getGenerationMode(), detailLevel, region.getVerticalQuality()); // if the fileName was null that means the folder is inaccessible // for some reason if (fileName == null) { ClientProxy.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 { // 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(); 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; int isFull = 0; try (XZCompressorInputStream inputStream = new XZCompressorInputStream(new FileInputStream(oldFile))) { fileVersion = inputStream.read(); inputStream.skip(1); isFull = inputStream.read() & 0b10000000; 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. return; } if ((temp[1] & 0b10000000) != 0b10000000 && isFull == 0b10000000) { // 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 ClientProxy.LOGGER.error("LOD file write error. Attempted to overwrite complete region with incomplete one [" + fileName + "]"); return; } // 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 outputStream.write(temp); outputStream.close(); // 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 (Exception e) { ClientProxy.LOGGER.error("LOD file write error. Unable to write to [" + fileName + "] error [" + e.getMessage() + "]: "); e.printStackTrace(); } } } //================// // helper methods // //================// public byte[] getHashFromFile(byte detailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) { int regionX = regionPos.x; int regionZ = regionPos.z; String fileName = getFileNameAndPathForRegion(regionX, regionZ, generationMode, detailLevel, verticalQuality); try (InputStream is = Files.newInputStream(Paths.get(fileName))) { return org.apache.commons.codec.digest.DigestUtils.md5(is); } catch (IOException ioEx) { ClientProxy.LOGGER.error("LOD file read error. Unable to read to [" + fileName + "] error [" + ioEx.getMessage() + "]: "); ioEx.printStackTrace(); } return new byte[0]; } /** * Return the name of the file that should contain the * region at the given x and z.
* Returns null if this object isn't available to read and write.

*

* example: "lod.0.0.txt"
*

* 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) { ClientProxy.LOGGER.warn("Unable to get the filename for the region [" + regionX + ", " + regionZ + "], error: [" + e.getMessage() + "], stacktrace: "); e.printStackTrace(); return null; } } }