diff --git a/src/main/java/com/seibel/lod/core/api/ClientApi.java b/src/main/java/com/seibel/lod/core/api/ClientApi.java index 6ccf56379..6817f4812 100644 --- a/src/main/java/com/seibel/lod/core/api/ClientApi.java +++ b/src/main/java/com/seibel/lod/core/api/ClientApi.java @@ -27,8 +27,6 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.lwjgl.glfw.GLFW; import com.seibel.lod.core.ModInfo; @@ -170,6 +168,12 @@ public class ClientApi ApiShared.lodBuilder.defaultDimensionWidthInRegions); ApiShared.lodWorld.addLodDimension(lodDim); } + // if necessary attempt to get the world file handler + if (lodDim.isFileHandlerNull()) + { + lodDim.attemptToSetWorldFileHandler(); + } + if (prefLoggerEnabled) { lodDim.dumpRamUsage(); lodBufferBuilderFactory.dumpBufferMemoryUsage(); diff --git a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java index b18fa9e54..8e6c241e3 100644 --- a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java +++ b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java @@ -163,8 +163,8 @@ public class LodBuilder // this happens if a LOD is generated after the user leaves the world. if (MC.getWrappedClientWorld() == null) return false; - if (!chunk.isLightCorrect()) return false; - if (!chunk.doesNearbyChunksExist()) return false; + if (!canGenerateLodFromChunk(chunk)) + return false; // generate the LODs @@ -190,8 +190,8 @@ public class LodBuilder data[i*maxVerticalData] = DataPointUtil.createVoidDataPoint(config.distanceGenerationMode.complexity); } } - if (!chunk.isLightCorrect()) return false; - if (!chunk.doesNearbyChunksExist()) return false; + if (!canGenerateLodFromChunk(chunk)) // TODO Why are we calling this again? - James + return false; if (genAll) { return writeAllLodNodeData(lodDim, region, chunk.getChunkPosX(), chunk.getChunkPosZ(), data, config, override); @@ -202,8 +202,12 @@ public class LodBuilder ApiShared.LOGGER.error("LodBuilder encountered an error on building lod: ", e); return false; } - } + public static boolean canGenerateLodFromChunk(IChunkWrapper chunk) + { + return chunk != null && chunk.isLightCorrect() && chunk.doesNearbyChunksExist(); + } + private boolean writeAllLodNodeData(LodDimension lodDim, LodRegion region, int chunkX, int chunkZ, long[] data, LodBuilderConfig config, boolean override) 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 515b2186a..33e9a1efe 100644 --- a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java +++ b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java @@ -37,7 +37,6 @@ import com.seibel.lod.core.api.ApiShared; import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; -import com.seibel.lod.core.api.ClientApi; import com.seibel.lod.core.enums.config.DistanceGenerationMode; import com.seibel.lod.core.enums.config.VerticalQuality; import com.seibel.lod.core.objects.lod.LevelContainer; @@ -77,6 +76,8 @@ public class LodDimensionFileHandler /** detail- */ private static final String DETAIL_FOLDER_NAME_PREFIX = "detail-"; + public static final String MULTIPLAYER_FOLDER_NAME = "Distant_Horizons_server_data"; + /** * .tmp
* Added to the end of the file path when saving to prevent diff --git a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHelper.java b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHelper.java new file mode 100644 index 000000000..08817b7e2 --- /dev/null +++ b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHelper.java @@ -0,0 +1,282 @@ +package com.seibel.lod.core.handlers; + +import com.seibel.lod.core.api.ApiShared; +import com.seibel.lod.core.builders.lodBuilding.LodBuilder; +import com.seibel.lod.core.builders.lodBuilding.LodBuilderConfig; +import com.seibel.lod.core.enums.config.DistanceGenerationMode; +import com.seibel.lod.core.enums.config.VerticalQuality; +import com.seibel.lod.core.handlers.dependencyInjection.SingletonHandler; +import com.seibel.lod.core.objects.lod.LodDimension; +import com.seibel.lod.core.objects.lod.LodRegion; +import com.seibel.lod.core.objects.lod.RegionPos; +import com.seibel.lod.core.util.LodUtil; +import com.seibel.lod.core.wrapperInterfaces.chunk.AbstractChunkPosWrapper; +import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper; +import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; +import com.seibel.lod.core.wrapperInterfaces.world.IDimensionTypeWrapper; +import com.seibel.lod.core.wrapperInterfaces.world.IWorldWrapper; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +/** + * Used to guess the world folder for the player's current dimension. + * @author James Seibel + * @version 3-17-2022 + */ +public class LodDimensionFileHelper +{ + private static final IMinecraftClientWrapper MC = SingletonHandler.get(IMinecraftClientWrapper.class); + + /** Increasing this will increase accuracy but increase calculation time */ + private static final VerticalQuality VERTICAL_QUALITY_TO_TEST_WITH = VerticalQuality.LOW; + + /** + * The minimum percent of identical dataPoints to consider two chunks as the same.
+ * 0.9 = 90% + */ + private static final double minimumSimilarityRequired = 0.9; + + + /** + * Currently this method checks a single chunk (where the player is) + * and compares it against the same chunk position in the other dimension worlds to + * guess which world the player is in. + * @return the new or existing folder for this dimension, null if there was a problem + * @throws IOException if the folder doesn't exist or can't be accessed + */ + public static File determineSaveFolder() throws IOException + { + // relevant positions + AbstractChunkPosWrapper playerChunkPos = MC.getPlayerChunkPos(); + int startingBlockPosX = playerChunkPos.getMinBlockX(); + int startingBlockPosZ = playerChunkPos.getMinBlockZ(); + RegionPos playerRegionPos = new RegionPos(MC.getPlayerChunkPos()); + + // chunk from the newly loaded dimension + IChunkWrapper newlyLoadedChunk = MC.getWrappedClientWorld().tryGetChunk(playerChunkPos); + // check if this chunk is valid to test + if (!LodDimensionFileHelper.CanDetermineDimensionFolder(newlyLoadedChunk)) + return null; + + // create a temporary dimension to store the new LOD + LodDimension newlyLoadedDim = new LodDimension(null, null, 1); + newlyLoadedDim.move(playerRegionPos); + newlyLoadedDim.regions.set(playerRegionPos.x, playerRegionPos.z, new LodRegion(LodUtil.BLOCK_DETAIL_LEVEL, playerRegionPos, VERTICAL_QUALITY_TO_TEST_WITH)); + + // generate a LOD to test against + boolean lodGenerated = ApiShared.lodBuilder.generateLodNodeFromChunk(newlyLoadedDim, newlyLoadedChunk, new LodBuilderConfig(DistanceGenerationMode.FULL), true, true); + if (!lodGenerated) + return null; + + // new chunk data + long[][][] newChunkData = new long[LodUtil.CHUNK_WIDTH][LodUtil.CHUNK_WIDTH][]; + for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) + { + for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + { + long[] array = newlyLoadedDim.getRegion(playerRegionPos.x, playerRegionPos.z).getAllData(LodUtil.BLOCK_DETAIL_LEVEL, x + startingBlockPosX, z + startingBlockPosZ); + newChunkData[x][z] = array; + } + } + boolean newChunkHasData = isDataEmpty(newChunkData); +// String message = "new chunk data " + (newChunkHasData ? newChunkData[0][0][0] : "[NULL]"); +// MC.sendChatMessage(message); +// ApiShared.LOGGER.info(message); + + // check if the chunk is actually empty + if (!newChunkHasData) + { + if (newlyLoadedChunk.getHeight() != 0) + { + // the chunk isn't empty but the LOD is... + +// String message = "Error: the chunk at (" + playerChunkPos.getX() + "," + playerChunkPos.getZ() + ") has a height of [" + newlyLoadedChunk.getHeight() + "] but the LOD generated is empty!"; +// MC.sendChatMessage(message); +// ApiShared.LOGGER.info(message); + return null; + } + else + { +// String message = "The chunk at (" + playerChunkPos.getX() + "," + playerChunkPos.getZ() + ") is empty."; +// MC.sendChatMessage(message); +// ApiShared.LOGGER.info(message); + } + } + + + + + // get every folder (world) we have for this dimension + File dimensionFolder = GetDimensionFolder(newlyLoadedDim.dimension, ""); + // check if the folder exists + if (dimensionFolder.listFiles() == null) + { + if (!dimensionFolder.exists()) + { + // create the directory since it doesn't exist + dimensionFolder.mkdirs(); + } + else + { + return null; + } + } + + + // compare each world with the newly loaded one + File mostSimilarWorldFolder = null; + int mostEqualLines = 0; + boolean oneDimensionIsValid = false; + + for (File testDimFolder : dimensionFolder.listFiles()) + { + // get a LOD from this dimension folder + LodDimension tempLodDim = new LodDimension(null, null, 1); + tempLodDim.move(playerRegionPos); + LodDimensionFileHandler tempFileHandler = new LodDimensionFileHandler(testDimFolder, tempLodDim); + LodRegion testRegion = tempFileHandler.loadRegionFromFile(LodUtil.BLOCK_DETAIL_LEVEL, playerRegionPos, VERTICAL_QUALITY_TO_TEST_WITH); + // get data from this LOD + long[][][] testChunkData = new long[LodUtil.CHUNK_WIDTH][LodUtil.CHUNK_WIDTH][]; + for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) + { + for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + { + long[] array = testRegion.getAllData(LodUtil.BLOCK_DETAIL_LEVEL, x + startingBlockPosX, z + startingBlockPosZ); + testChunkData[x][z] = array; + } + } + + // check if the chunk is actually empty + if (!isDataEmpty(newChunkData)) + { +// String message = "The test chunk for dimension folder [" + testDimFolder.getName() + "] and chunk pos (" + playerChunkPos.getX() + "," + playerChunkPos.getZ() + ") is empty. Is that correct?"; +// MC.sendChatMessage(message); +// ApiShared.LOGGER.info(message); + continue; + } + oneDimensionIsValid = true; + + + // compare the two LODs + int equalLines = 0; + int totalLineCount = 0; + for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) + { + for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + { + for (int y = 0; y < newChunkData[x][y].length; y++) + { + if (newChunkData[x][z][y] == testChunkData[x][z][y]) + { + equalLines++; + } + totalLineCount++; + } + } + } + +// String message = "test data [" + testDimFolder.getName().substring(0, 6) + "...] " + testChunkData[0][0][0] + " equal lines: " + equalLines + "/" + totalLineCount; +// MC.sendChatMessage(message); +// ApiShared.LOGGER.info(message); + + // determine if this world is closer to the newly loaded world + double percentEqual = (double) equalLines / (double) totalLineCount; + if (equalLines > mostEqualLines && percentEqual >= minimumSimilarityRequired) + { + mostEqualLines = equalLines; + mostSimilarWorldFolder = testDimFolder; + } + } + + + if (!oneDimensionIsValid && dimensionFolder.listFiles().length != 0) + // all the world folders were empty, and there was at least one world folder that we tested + return null; + + + if (mostSimilarWorldFolder != null) + { + // we found a world folder that is similar, use it + + String message = "Dimension folder set to: [" + mostSimilarWorldFolder.getName().substring(0, 8) + "...]"; +// MC.sendChatMessage(message); + ApiShared.LOGGER.info(message); + return mostSimilarWorldFolder; + } + else + { + // no world folder was found, create a new one + + String newId = UUID.randomUUID().toString(); + String message = "No dimension folder found. Creating a new one with ID: " + newId.substring(0, 8) + "..."; +// MC.sendChatMessage(message); + ApiShared.LOGGER.info(message); + return GetDimensionFolder(newlyLoadedDim.dimension, newId); + } + } + + /** + * Returns the dimension folder with the specific ID if specified.
+ * If the worldId is empty or null this returns the dimension parent folder
+ * Example folder names: "dim_overworld/worldId", "dim_the_nether/worldId" + */ + public static File GetDimensionFolder(IDimensionTypeWrapper newDimensionType, String worldId) + { + // prevent null pointers + if (worldId == null) + worldId = ""; + + try + { + if (MC.hasSinglePlayerServer()) + { + // local world + IWorldWrapper serverWorld = LodUtil.getServerWorldFromDimension(newDimensionType); + return new File(serverWorld.getSaveFolder().getCanonicalFile().getPath() + File.separatorChar + "lod" + File.separatorChar + worldId); + } + else + { + // multiplayer + return new File(MC.getGameDirectory().getCanonicalFile().getPath() + + File.separatorChar + "Distant_Horizons_server_data" + File.separatorChar + MC.getCurrentDimensionId() + File.separatorChar + worldId); + } + } + catch (IOException e) + { + ApiShared.LOGGER.error("Unable to get dimension folder for dimension [" + newDimensionType.getDimensionName() + "]", e); + return null; + } + } + + + /** Returns true if the given chunk is valid to test */ + public static boolean CanDetermineDimensionFolder(IChunkWrapper chunk) + { + // we can only guess if the given chunk can be converted into a LOD + return LodBuilder.canGenerateLodFromChunk(chunk); + } + + + /** Used for debugging, returns true if every data point is 0 */ + private static boolean isDataEmpty(long[][][] chunkData) + { + for (long[][] xArray : chunkData) + { + for (long[] zArray : xArray) + { + for (long dataPoint : zArray) + { + if (dataPoint != 0) + { + return true; + } + } + } + } + + return false; + } + +} diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java index 62038d099..3578b5154 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java @@ -19,6 +19,23 @@ package com.seibel.lod.core.objects.lod; +import com.seibel.lod.core.api.ApiShared; +import com.seibel.lod.core.api.ClientApi; +import com.seibel.lod.core.enums.config.DistanceGenerationMode; +import com.seibel.lod.core.enums.config.DropoffQuality; +import com.seibel.lod.core.enums.config.GenerationPriority; +import com.seibel.lod.core.enums.config.VerticalQuality; +import com.seibel.lod.core.handlers.LodDimensionFileHandler; +import com.seibel.lod.core.handlers.LodDimensionFileHelper; +import com.seibel.lod.core.handlers.dependencyInjection.SingletonHandler; +import com.seibel.lod.core.objects.PosToGenerateContainer; +import com.seibel.lod.core.util.*; +import com.seibel.lod.core.util.MovableGridRingList.Pos; +import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory; +import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton; +import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; +import com.seibel.lod.core.wrapperInterfaces.world.IDimensionTypeWrapper; + import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -26,33 +43,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import com.seibel.lod.core.api.ApiShared; -import com.seibel.lod.core.api.ClientApi; -import com.seibel.lod.core.builders.lodBuilding.LodBuilderConfig; -import com.seibel.lod.core.enums.config.DistanceGenerationMode; -import com.seibel.lod.core.enums.config.DropoffQuality; -import com.seibel.lod.core.enums.config.GenerationPriority; -import com.seibel.lod.core.enums.config.VerticalQuality; -import com.seibel.lod.core.handlers.LodDimensionFileHandler; -import com.seibel.lod.core.handlers.dependencyInjection.SingletonHandler; -import com.seibel.lod.core.objects.PosToGenerateContainer; -import com.seibel.lod.core.util.DataPointUtil; -import com.seibel.lod.core.util.DetailDistanceUtil; -import com.seibel.lod.core.util.LevelPosUtil; -import com.seibel.lod.core.util.LodThreadFactory; -import com.seibel.lod.core.util.LodUtil; -import com.seibel.lod.core.util.MovableGridRingList; -import com.seibel.lod.core.util.MovableGridRingList.Pos; -import com.seibel.lod.core.util.SpamReducedLogger; -import com.seibel.lod.core.util.UnitBytes; -import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory; -import com.seibel.lod.core.wrapperInterfaces.chunk.AbstractChunkPosWrapper; -import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper; -import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton; -import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; -import com.seibel.lod.core.wrapperInterfaces.world.IDimensionTypeWrapper; -import com.seibel.lod.core.wrapperInterfaces.world.IWorldWrapper; - //FIXME: Race condition on lodDim move/resize! @@ -66,7 +56,7 @@ import com.seibel.lod.core.wrapperInterfaces.world.IWorldWrapper; * * @author Leonardo Amato * @author James Seibel - * @version 11-12-2021 + * @version 3-17-2022 */ public class LodDimension { @@ -89,7 +79,8 @@ public class LodDimension private volatile RegionPos[] iteratorList = null; - private LodDimensionFileHandler fileHandler; + private LodDimensionFileHandler fileHandler = null; + public boolean isFileHandlerNull() { return this.fileHandler == null; } public volatile int dirtiedRegionsRoughCount = 0; @@ -107,37 +98,12 @@ public class LodDimension public LodDimension(IDimensionTypeWrapper newDimension, LodWorld lodWorld, int newWidth) { dimension = newDimension; - width = newWidth; + width = newWidth; // FIXME any width besides 1 causes an indexOutOfBounds Exception halfWidth = width / 2; if (newDimension != null && lodWorld != null) { - try - { - // determine the save folder - File saveDir; - if (MC.hasSinglePlayerServer()) - { - // local world - - IWorldWrapper serverWorld = LodUtil.getServerWorldFromDimension(newDimension); - saveDir = new File(serverWorld.getSaveFolder().getCanonicalFile().getPath() + File.separatorChar + "lod"); - } - else - { - // connected to server - - saveDir = new File(MC.getGameDirectory().getCanonicalFile().getPath() + - File.separatorChar + "Distant_Horizons_server_data" + File.separatorChar + MC.getCurrentDimensionId()); - } - - fileHandler = new LodDimensionFileHandler(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 - } + attemptToSetWorldFileHandler(); } @@ -145,6 +111,40 @@ public class LodDimension generateIteratorList(); } + /** + * Attempts to determine and set the file handler based on + * the chunk the player is currently in. + * @returns true if the fileHandler has been set, false otherwise + */ + public boolean attemptToSetWorldFileHandler() + { + // check if we need to get the file handler + if (this.fileHandler != null) + return true; + + try + { + // attempt to get the file handler + File saveDir = LodDimensionFileHelper.determineSaveFolder(); + if (saveDir == null) + return false; + + this.fileHandler = new LodDimensionFileHandler(saveDir, this); + + // clear the previous regions so we can load the LODs from file + if (this.regions != null) + this.regions.clear(); + return true; + } + catch(IOException e) + { + ApiShared.LOGGER.error("Unable to set the dimension file handler for dimension type [" + this.dimension.getDimensionName() + "]. Error: " + e.getMessage(), e); + return false; + } + } + + + private void generateIteratorList() { iteratorList = null; diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java index 85a963c9a..de5b86f09 100644 --- a/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java @@ -20,12 +20,13 @@ package com.seibel.lod.core.wrapperInterfaces.chunk; import com.seibel.lod.core.handlers.dependencyInjection.IBindable; +import com.seibel.lod.core.util.LodUtil; import com.seibel.lod.core.wrapperInterfaces.block.IBlockDetailWrapper; import com.seibel.lod.core.wrapperInterfaces.world.IBiomeWrapper; /** * @author James Seibel - * @version 11-17-2021 + * @version 3-16-2022 */ public interface IChunkWrapper extends IBindable { @@ -70,4 +71,23 @@ public interface IChunkWrapper extends IBindable } boolean doesNearbyChunksExist(); + + + + /** This is a bad hash algorithm, but can be used for rough debugging. */ + public default int roughHashCode() + { + int hash = 31; + int primeMultiplier = 227; + + for(int x = 0; x < LodUtil.CHUNK_WIDTH; x++) + { + for(int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + { + hash = hash * primeMultiplier + Integer.hashCode(getMaxY(x, z)); + } + } + + return hash; + } }