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;
+ }
}