575 lines
17 KiB
Java
575 lines
17 KiB
Java
package com.seibel.lod.objects;
|
|
|
|
|
|
import com.seibel.lod.config.LodConfig;
|
|
import com.seibel.lod.enums.DistanceGenerationMode;
|
|
import com.seibel.lod.enums.LodTemplate;
|
|
import com.seibel.lod.enums.VerticalQuality;
|
|
import com.seibel.lod.util.DataPointUtil;
|
|
import com.seibel.lod.util.DetailDistanceUtil;
|
|
import com.seibel.lod.util.LevelPosUtil;
|
|
import com.seibel.lod.util.LodUtil;
|
|
|
|
/**
|
|
* This object holds all loaded LevelContainers
|
|
* for a given region. <Br><Br>
|
|
*
|
|
* <strong>Coordinate Standard: </strong><br>
|
|
* Coordinate called posX or posZ are relative LevelPos coordinates <br>
|
|
* unless stated otherwise. <br>
|
|
*
|
|
* @author Leonardo Amato
|
|
* @version 9-27-2021
|
|
*/
|
|
public class LodRegion
|
|
{
|
|
/**
|
|
* TODO what does this represent, and should it be defined here?
|
|
*/
|
|
private static final byte POSSIBLE_LOD = 10;
|
|
|
|
|
|
/**
|
|
* Holds the lowest (least detailed) detail level in this region
|
|
* TODO is that correct?
|
|
*/
|
|
private byte minDetailLevel;
|
|
|
|
/**
|
|
* This holds all data for this region
|
|
*/
|
|
private LevelContainer[] dataContainer;
|
|
|
|
/**
|
|
* the generation mode for this region
|
|
* TODO will this ever change through a region's life?
|
|
*/
|
|
private DistanceGenerationMode generationMode;
|
|
/**
|
|
* the vertical quality of this region
|
|
*/
|
|
private VerticalQuality verticalQuality;
|
|
|
|
/**
|
|
* this region's x RegionPos
|
|
*/
|
|
public final int regionPosX;
|
|
/**
|
|
* this region's z RegionPos
|
|
*/
|
|
public final int regionPosZ;
|
|
|
|
|
|
public LodRegion(RegionPos regionPos)
|
|
{
|
|
this.minDetailLevel = LodUtil.REGION_DETAIL_LEVEL;
|
|
this.regionPosX = regionPos.x;
|
|
this.regionPosZ = regionPos.z;
|
|
dataContainer = new LevelContainer[POSSIBLE_LOD];
|
|
}
|
|
|
|
public LodRegion(byte minDetailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, VerticalQuality verticalQuality)
|
|
{
|
|
this.minDetailLevel = minDetailLevel;
|
|
this.regionPosX = regionPos.x;
|
|
this.regionPosZ = regionPos.z;
|
|
this.verticalQuality = verticalQuality;
|
|
this.generationMode = generationMode;
|
|
dataContainer = new LevelContainer[POSSIBLE_LOD];
|
|
|
|
|
|
// Initialize all the different matrices
|
|
for (byte lod = minDetailLevel; lod <= LodUtil.REGION_DETAIL_LEVEL; lod++)
|
|
{
|
|
switch (verticalQuality)
|
|
{
|
|
default:
|
|
case HEIGHTMAP:
|
|
dataContainer[lod] = new SingleLevelContainer(lod);
|
|
break;
|
|
case VOXEL:
|
|
dataContainer[lod] = new VerticalLevelContainer(lod);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Inserts the data point into the region.
|
|
* <p>
|
|
* TODO this will always return true unless it has
|
|
*
|
|
* @return true if the data was added successfully
|
|
*/
|
|
public boolean addData(byte detailLevel, int posX, int posZ, int verticalIndex, long data)
|
|
{
|
|
posX = LevelPosUtil.getRegionModule(detailLevel, posX);
|
|
posZ = LevelPosUtil.getRegionModule(detailLevel, posZ);
|
|
|
|
// The dataContainer could have null entries if the
|
|
// detailLevel changes.
|
|
if (this.dataContainer[detailLevel] == null)
|
|
{
|
|
if (verticalQuality == VerticalQuality.HEIGHTMAP)
|
|
this.dataContainer[detailLevel] = new SingleLevelContainer(detailLevel);
|
|
else
|
|
this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel);
|
|
}
|
|
|
|
this.dataContainer[detailLevel].addData(data, posX, posZ, verticalIndex);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get the dataPoint at the given relative position.
|
|
*
|
|
* @return the data at the relative pos and detail level,
|
|
* 0 if the data doesn't exist.
|
|
*/
|
|
public long getData(byte detailLevel, int posX, int posZ, int verticalIndex)
|
|
{
|
|
return dataContainer[detailLevel].getData(posX, posZ, verticalIndex);
|
|
}
|
|
|
|
/**
|
|
* Get the dataPoint at the given relative position.
|
|
*
|
|
* @return the data at the relative pos and detail level,
|
|
* 0 if the data doesn't exist.
|
|
*/
|
|
public long getSingleData(byte detailLevel, int posX, int posZ)
|
|
{
|
|
return dataContainer[detailLevel].getSingleData(posX, posZ);
|
|
}
|
|
|
|
/**
|
|
* Clears the datapoint at the given relative position
|
|
*/
|
|
public void clear(byte detailLevel, int posX, int posZ)
|
|
{
|
|
dataContainer[detailLevel].clear(posX, posZ);
|
|
}
|
|
|
|
/**
|
|
* This method will fill the posToGenerate array with all levelPos that
|
|
* are render-able.
|
|
* <p>
|
|
* TODO why don't we return the posToGenerate, it would make this easier to understand
|
|
*/
|
|
public void getDataToGenerate(PosToGenerateContainer posToGenerate,
|
|
int playerBlockPosX, int playerBlockPosZ)
|
|
{
|
|
getDataToGenerate(posToGenerate, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerBlockPosX, playerBlockPosZ);
|
|
|
|
}
|
|
|
|
/**
|
|
* A recursive method that fills the posToGenerate array with all levelPos that
|
|
* need to be generated.
|
|
* <p>
|
|
* TODO why don't we return the posToGenerate, it would make this easier to understand
|
|
*/
|
|
private void getDataToGenerate(PosToGenerateContainer posToGenerate, byte detailLevel,
|
|
int childOffsetPosX, int childOffsetPosZ, int playerPosX, int playerPosZ)
|
|
{
|
|
// equivalent to 2^(...)
|
|
int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel);
|
|
|
|
// calculate what LevelPos are in range to generate
|
|
int maxDistance = LevelPosUtil.maxDistance(detailLevel, childOffsetPosX, childOffsetPosZ, playerPosX, playerPosZ, regionPosX, regionPosZ);
|
|
|
|
// determine this child's levelPos
|
|
byte childDetailLevel = (byte) (detailLevel - 1);
|
|
int childPosX = childOffsetPosX * 2;
|
|
int childPosZ = childOffsetPosZ * 2;
|
|
|
|
int childSize = 1 << (LodUtil.REGION_DETAIL_LEVEL - childDetailLevel);
|
|
|
|
byte targetDetailLevel = DetailDistanceUtil.getLodGenDetail(DetailDistanceUtil.getGenerationDetailFromDistance(maxDistance)).detailLevel;
|
|
if (targetDetailLevel > detailLevel)
|
|
{
|
|
// we have gone beyond the target Detail level
|
|
// we can stop generating
|
|
return;
|
|
} else if (targetDetailLevel == detailLevel)
|
|
{
|
|
if (!doesDataExist(detailLevel, childOffsetPosX, childOffsetPosZ))
|
|
posToGenerate.addPosToGenerate(detailLevel, childOffsetPosX + regionPosX * size, childOffsetPosZ + regionPosZ * size);
|
|
} else
|
|
{
|
|
// we want at max one request per chunk (since the world generator creates chunks).
|
|
// So for lod smaller than a chunk, only recurse down
|
|
// the top right child
|
|
|
|
if (detailLevel > LodUtil.CHUNK_DETAIL_LEVEL)
|
|
{
|
|
int ungeneratedChildren = 0;
|
|
|
|
// make sure all children are generated to this detailLevel
|
|
for (int x = 0; x <= 1; x++)
|
|
{
|
|
for (int z = 0; z <= 1; z++)
|
|
{
|
|
if (!doesDataExist(childDetailLevel, childPosX + x, childPosZ + z))
|
|
{
|
|
ungeneratedChildren++;
|
|
posToGenerate.addPosToGenerate(childDetailLevel, childPosX + x + regionPosX * childSize, childPosZ + z + regionPosZ * childSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
// only if all the children are correctly generated
|
|
// should we go deeper
|
|
if (ungeneratedChildren == 0)
|
|
for (int x = 0; x <= 1; x++)
|
|
for (int z = 0; z <= 1; z++)
|
|
getDataToGenerate(posToGenerate, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ);
|
|
} else
|
|
{
|
|
// The detail Level is smaller than a chunk.
|
|
// Only recurse down the top right child.
|
|
|
|
if (DetailDistanceUtil.getLodGenDetail(childDetailLevel).detailLevel <= (childDetailLevel))
|
|
{
|
|
if (!doesDataExist(childDetailLevel, childPosX, childPosZ))
|
|
posToGenerate.addPosToGenerate(childDetailLevel, childPosX + regionPosX * childSize, childPosZ + regionPosZ * childSize);
|
|
else
|
|
getDataToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* This method will fill the posToRender array with all levelPos that
|
|
* are render-able.
|
|
* <p>
|
|
* TODO why don't we return the posToRender, it would make this easier to understand
|
|
*/
|
|
public void getDataToRender(PosToRenderContainer posToRender,
|
|
int playerPosX, int playerPosZ, boolean requireCorrectDetailLevel)
|
|
{
|
|
getDataToRender(posToRender, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerPosX, playerPosZ, requireCorrectDetailLevel);
|
|
}
|
|
|
|
/**
|
|
* This method will fill the posToRender array with all levelPos that
|
|
* are render-able.
|
|
* <p>
|
|
* TODO why don't we return the posToRender, it would make this easier to understand
|
|
* TODO this needs some more comments, James was only able to figure out part of it
|
|
*/
|
|
private void getDataToRender(PosToRenderContainer posToRender,
|
|
byte detailLevel, int posX, int posZ,
|
|
int playerPosX, int playerPosZ, boolean requireCorrectDetailLevel)
|
|
{
|
|
// equivalent to 2^(...)
|
|
int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel);
|
|
|
|
byte desiredLevel;
|
|
int maxDistance;
|
|
boolean stopNow = false;
|
|
int minDistance;
|
|
int childLevel;
|
|
|
|
|
|
// calculate the LevelPos that are in range
|
|
switch (LodConfig.CLIENT.graphics.detailDropOff.get())
|
|
{
|
|
|
|
case FAST:
|
|
int playerRegionX = LevelPosUtil.getRegion(LodUtil.BLOCK_DETAIL_LEVEL, playerPosX);
|
|
int playerRegionZ = LevelPosUtil.getRegion(LodUtil.BLOCK_DETAIL_LEVEL, playerPosZ);
|
|
if (playerRegionX == regionPosX && playerRegionZ == regionPosZ)
|
|
{
|
|
maxDistance = LevelPosUtil.maxDistance(detailLevel, posX, posZ, playerPosX, playerPosZ, regionPosX, regionPosZ);
|
|
desiredLevel = DetailDistanceUtil.getLodDrawDetail(DetailDistanceUtil.getDrawDetailFromDistance(maxDistance));
|
|
minDistance = LevelPosUtil.minDistance(detailLevel, posX, posZ, playerPosX, playerPosZ, regionPosX, regionPosZ);
|
|
childLevel = DetailDistanceUtil.getLodDrawDetail(DetailDistanceUtil.getDrawDetailFromDistance(minDistance));
|
|
stopNow = detailLevel == childLevel - 1;
|
|
break;
|
|
}
|
|
default:
|
|
case FANCY:
|
|
maxDistance = LevelPosUtil.maxDistance(detailLevel, posX, posZ, playerPosX, playerPosZ, regionPosX, regionPosZ);
|
|
desiredLevel = DetailDistanceUtil.getLodDrawDetail(DetailDistanceUtil.getDrawDetailFromDistance(maxDistance));
|
|
minDistance = LevelPosUtil.minDistance(detailLevel, posX, posZ, playerPosX, playerPosZ, regionPosX, regionPosZ);
|
|
childLevel = DetailDistanceUtil.getLodDrawDetail(DetailDistanceUtil.getDrawDetailFromDistance(minDistance));
|
|
stopNow = detailLevel == childLevel - 1;
|
|
break;
|
|
}
|
|
|
|
if (stopNow)
|
|
{
|
|
posToRender.addPosToRender(detailLevel,
|
|
posX + regionPosX * size,
|
|
posZ + regionPosZ * size);
|
|
} else if (desiredLevel > detailLevel)
|
|
{
|
|
// we have gone beyond the target Detail level
|
|
// we can stop generating
|
|
return;
|
|
} else if (desiredLevel == detailLevel)
|
|
{
|
|
posToRender.addPosToRender(detailLevel,
|
|
posX + regionPosX * size,
|
|
posZ + regionPosZ * size);
|
|
} else //case where (detailLevel > desiredLevel)
|
|
{
|
|
int childPosX = posX * 2;
|
|
int childPosZ = posZ * 2;
|
|
byte childDetailLevel = (byte) (detailLevel - 1);
|
|
int childrenCount = 0;
|
|
|
|
for (int x = 0; x <= 1; x++)
|
|
{
|
|
for (int z = 0; z <= 1; z++)
|
|
{
|
|
if (doesDataExist(childDetailLevel, childPosX + x, childPosZ + z))
|
|
{
|
|
if (!requireCorrectDetailLevel)
|
|
childrenCount++;
|
|
else
|
|
getDataToRender(posToRender, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ, requireCorrectDetailLevel);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (!requireCorrectDetailLevel)
|
|
{
|
|
// If all the four children exist go deeper
|
|
if (childrenCount == 4)
|
|
{
|
|
for (int x = 0; x <= 1; x++)
|
|
for (int z = 0; z <= 1; z++)
|
|
getDataToRender(posToRender, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ, requireCorrectDetailLevel);
|
|
} else
|
|
{
|
|
posToRender.addPosToRender(detailLevel,
|
|
posX + regionPosX * size,
|
|
posZ + regionPosZ * size);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates all children.
|
|
* <p>
|
|
* TODO could this be renamed mergeArea?
|
|
*/
|
|
public void updateArea(byte detailLevel, int posX, int posZ)
|
|
{
|
|
int width;
|
|
int startX;
|
|
int startZ;
|
|
|
|
// TODO what are each of these loops updating?
|
|
for (byte down = (byte) (minDetailLevel + 1); down <= detailLevel; down++)
|
|
{
|
|
startX = LevelPosUtil.convert(detailLevel, posX, down);
|
|
startZ = LevelPosUtil.convert(detailLevel, posZ, down);
|
|
width = 1 << (detailLevel - down);
|
|
|
|
for (int x = 0; x < width; x++)
|
|
for (int z = 0; z < width; z++)
|
|
update(down, startX + x, startZ + z);
|
|
}
|
|
|
|
|
|
for (byte up = (byte) (detailLevel + 1); up <= LodUtil.REGION_DETAIL_LEVEL; up++)
|
|
{
|
|
update(up,
|
|
LevelPosUtil.convert(detailLevel, posX, up),
|
|
LevelPosUtil.convert(detailLevel, posZ, up));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the child at the given relative Pos
|
|
* <p>
|
|
* TODO could this be renamed mergeChildData?
|
|
*/
|
|
private void update(byte detailLevel, int posX, int posZ)
|
|
{
|
|
dataContainer[detailLevel].updateData(dataContainer[detailLevel - 1], posX, posZ);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns if data exists at the given relative Pos.
|
|
*/
|
|
public boolean doesDataExist(byte detailLevel, int posX, int posZ)
|
|
{
|
|
if (detailLevel < minDetailLevel)
|
|
return false;
|
|
|
|
posX = LevelPosUtil.getRegionModule(detailLevel, posX);
|
|
posZ = LevelPosUtil.getRegionModule(detailLevel, posZ);
|
|
|
|
if (dataContainer == null || dataContainer[detailLevel] == null)
|
|
return false;
|
|
|
|
return dataContainer[detailLevel].doesItExist(posX, posZ);
|
|
}
|
|
|
|
/**
|
|
* Gets the generation mode for the data point at the given relative pos.
|
|
*/
|
|
public byte getGenerationMode(byte detailLevel, int posX, int posZ)
|
|
{
|
|
if (dataContainer[detailLevel].doesItExist(posX, posZ))
|
|
// We take the bottom information always
|
|
// TODO what does that mean? bottom of what?
|
|
return DataPointUtil.getGenerationMode(dataContainer[detailLevel].getSingleData(posX, posZ));
|
|
else
|
|
return DistanceGenerationMode.NONE.complexity;
|
|
}
|
|
|
|
/**
|
|
* Returns the lowest detail level in this region
|
|
* TODO is that right?
|
|
*/
|
|
public byte getMinDetailLevel()
|
|
{
|
|
return minDetailLevel;
|
|
}
|
|
|
|
/**
|
|
* Returns the LevelContainer for the detailLevel
|
|
*
|
|
* @throws IllegalArgumentException if the detailLevel is less than minDetailLevel
|
|
*/
|
|
public LevelContainer getLevel(byte detailLevel)
|
|
{
|
|
if (detailLevel < minDetailLevel)
|
|
throw new IllegalArgumentException("getLevel asked for a detail level that does not exist: minimum: [" + minDetailLevel + "] level requested: [" + detailLevel + "]");
|
|
|
|
return dataContainer[detailLevel];
|
|
}
|
|
|
|
/**
|
|
* Add the levelContainer to this Region, updating the minDetailLevel
|
|
* if necessary.
|
|
*
|
|
* @throws IllegalArgumentException if the LevelContainer's detailLevel
|
|
* is 2 or more detail levels lower than the
|
|
* minDetailLevel of this region.
|
|
*/
|
|
public void addLevelContainer(LevelContainer levelContainer)
|
|
{
|
|
if (levelContainer.getDetailLevel() < minDetailLevel - 1)
|
|
{
|
|
throw new IllegalArgumentException(
|
|
"the LevelContainer's detailLevel was "
|
|
+ "[" + levelContainer.getDetailLevel() + "] but this region "
|
|
+ "only allows adding LevelContainers with a "
|
|
+ "detail level of [" + (minDetailLevel - 1) + "]");
|
|
}
|
|
|
|
if (levelContainer.getDetailLevel() == minDetailLevel - 1)
|
|
minDetailLevel = levelContainer.getDetailLevel();
|
|
|
|
dataContainer[levelContainer.getDetailLevel()] = levelContainer;
|
|
}
|
|
|
|
// TODO James thinks cutTree and growTree (which he renamed to match cutTree)
|
|
// should have more descriptive names, to make sure the "Tree" portion isn't
|
|
// confused with Minecraft trees (the plant).
|
|
|
|
/**
|
|
* Removes any dataContainers that are higher than
|
|
* the given detailLevel
|
|
*/
|
|
public void cutTree(byte detailLevel)
|
|
{
|
|
if (detailLevel > minDetailLevel)
|
|
{
|
|
for (byte detailLevelIndex = 0; detailLevelIndex < detailLevel; detailLevelIndex++)
|
|
dataContainer[detailLevelIndex] = null;
|
|
|
|
minDetailLevel = detailLevel;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make this region more detailed to the detailLevel given.
|
|
* TODO is that correct?
|
|
*/
|
|
public void growTree(byte detailLevel)
|
|
{
|
|
if (detailLevel < minDetailLevel)
|
|
{
|
|
for (byte detailLevelIndex = (byte) (minDetailLevel - 1); detailLevelIndex >= detailLevel; detailLevelIndex--)
|
|
{
|
|
if (dataContainer[detailLevelIndex + 1] == null)
|
|
dataContainer[detailLevelIndex + 1] = new SingleLevelContainer((byte) (detailLevelIndex + 1));
|
|
|
|
dataContainer[detailLevelIndex] = dataContainer[detailLevelIndex + 1].expand();
|
|
}
|
|
minDetailLevel = detailLevel;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* return RegionPos of this lod region
|
|
*/
|
|
public RegionPos getRegionPos()
|
|
{
|
|
return new RegionPos(regionPosX, regionPosZ);
|
|
}
|
|
|
|
/**
|
|
* Returns the minimum memory needed in bytes
|
|
*/
|
|
public int getMinMemoryNeeded(LodTemplate template)
|
|
{
|
|
int memory = 0;
|
|
for (byte detailLevelIndex = LodUtil.REGION_DETAIL_LEVEL; detailLevelIndex > minDetailLevel; detailLevelIndex--)
|
|
{
|
|
// TODO why are we multiplying the dataContainer's memory by the template's memory?
|
|
memory += dataContainer[detailLevelIndex].getMaxMemoryUse() * template.getBufferMemoryForSingleLod(dataContainer[detailLevelIndex].getMaxVerticalData());
|
|
}
|
|
return memory;
|
|
}
|
|
|
|
/**
|
|
* Returns how many LODs are in this region
|
|
*/
|
|
public int getNumberOfLods()
|
|
{
|
|
int count = 0;
|
|
for (LevelContainer container : dataContainer)
|
|
count += container.getMaxNumberOfLods();
|
|
|
|
return count;
|
|
}
|
|
|
|
public VerticalQuality getVerticalQuality()
|
|
{
|
|
return verticalQuality;
|
|
}
|
|
|
|
public DistanceGenerationMode getGenerationMode()
|
|
{
|
|
return generationMode;
|
|
}
|
|
|
|
public int getMaxVerticalData(byte detailLevel)
|
|
{
|
|
return dataContainer[detailLevel].getMaxVerticalData();
|
|
}
|
|
|
|
|
|
@Override
|
|
public String toString()
|
|
{
|
|
return getLevel(LodUtil.REGION_DETAIL_LEVEL).toString();
|
|
}
|
|
}
|