Closes #134 (Add multi dimension support)

I still need to add a config and multithreading (to prevent stuttering when changing dimensions).
This commit is contained in:
James Seibel
2022-03-17 23:08:42 -05:00
parent 78a1cc3330
commit c869047b30
6 changed files with 376 additions and 65 deletions
@@ -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();
@@ -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)
@@ -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 <br>
* Added to the end of the file path when saving to prevent
@@ -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. <Br>
* 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. <br>
* If the worldId is empty or null this returns the dimension parent folder <br>
* 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;
}
}
@@ -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;
@@ -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;
}
}