Refactor, comment, and clean up everything

This adds comments to almost every class, removes a few classes that weren't being used, improves the names of a number of methods, and does a bunch of random polishing/cleaning.
This commit is contained in:
James Seibel
2021-02-23 08:19:37 -06:00
parent 7b074fc155
commit 2ee76a413f
20 changed files with 424 additions and 372 deletions
@@ -16,6 +16,10 @@ import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin;
/**
* Initialize and setup the Mod.
* <br>
* If you are looking for the real start of the mod
* check out the ClientProxy.
*
* @author James Seibel
* @version 02-07-2021
@@ -5,7 +5,6 @@ import java.util.concurrent.Callable;
import org.lwjgl.opengl.GL11;
import com.backsun.lod.objects.NearFarBuffer;
import com.backsun.lod.renderer.RenderUtil;
import com.backsun.lod.util.enums.FogDistance;
import net.minecraft.client.renderer.BufferBuilder;
@@ -13,10 +12,12 @@ import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.math.AxisAlignedBB;
/**
*
* This object is used to create NearFarBuffer objects
* in a thread independent way, so multiple of these objects can be
* created and executed in parallel to populate BufferBuilders.
*
* @author James Seibel
* @version 02-21-2021
* @version 02-22-2021
*/
public class BuildBufferThread implements Callable<NearFarBuffer>
{
@@ -70,6 +71,9 @@ public class BuildBufferThread implements Callable<NearFarBuffer>
int blue;
int alpha;
// this is done if the FogDistance is either
// NEAR or FAR, if it is NEAR_AND_FAR
// the buffer is determined for each LOD
if (distanceMode == FogDistance.NEAR)
{
currentBuffer = nearBuffer;
@@ -97,10 +101,10 @@ public class BuildBufferThread implements Callable<NearFarBuffer>
blue = colors[i][j].getBlue();
alpha = colors[i][j].getAlpha();
// choose which buffer to add these LODs too
if (distanceMode == FogDistance.NEAR_AND_FAR)
{
if (RenderUtil.isCoordinateInNearFogArea(i, j, numbChunksWide / 2))
if (isCoordinateInNearFogArea(i, j, numbChunksWide / 2))
currentBuffer = nearBuffer;
else
currentBuffer = farBuffer;
@@ -193,6 +197,22 @@ public class BuildBufferThread implements Callable<NearFarBuffer>
{
buffer.pos(x, y, z).color(red, green, blue, alpha).endVertex();
}
/**
* Find the coordinates that are in the center half of the given
* 2D matrix, starting at (0,0) and going to (2 * lodRadius, 2 * lodRadius).
*/
private static boolean isCoordinateInNearFogArea(int chunkX, int chunkZ, int lodRadius)
{
int halfRadius = lodRadius / 2;
return (chunkX >= lodRadius - halfRadius
&& chunkX <= lodRadius + halfRadius)
&&
(chunkZ >= lodRadius - halfRadius
&& chunkZ <= lodRadius + halfRadius);
}
}
@@ -3,7 +3,7 @@ package com.backsun.lod.builders;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.backsun.lod.handlers.LodFileHandler;
import com.backsun.lod.handlers.LodDimensionFileHandler;
import com.backsun.lod.objects.LodChunk;
import com.backsun.lod.objects.LodDimension;
import com.backsun.lod.objects.LodWorld;
@@ -20,12 +20,12 @@ import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
* (specifically: Lod World, Dimension, Region, and Chunk objects)
*
* @author James Seibel
* @version 2-21-2021
* @version 2-22-2021
*/
public class LodBuilder
{
private ExecutorService lodGenThreadPool = Executors.newFixedThreadPool(1);
public LodWorld lodWorld;
private ExecutorService lodGenThreadPool = Executors.newSingleThreadExecutor();
public volatile LodWorld lodWorld;
/** Default size of any LOD regions we use */
public int regionWidth = 5;
@@ -41,7 +41,7 @@ public class LodBuilder
* Returns LodWorld so that it can be passed
* to the LodRenderer.
*/
public LodWorld generateLodChunk(Chunk chunk)
public LodWorld generateLodChunkAsync(Chunk chunk)
{
Minecraft mc = Minecraft.getMinecraft();
@@ -68,13 +68,13 @@ public class LodBuilder
if (lodWorld == null)
{
lodWorld = new LodWorld(LodFileHandler.getWorldName());
lodWorld = new LodWorld(LodDimensionFileHandler.getWorldName());
}
else
{
// if we have a lodWorld make sure
// it is for this minecraft world
if (!lodWorld.worldName.equals(LodFileHandler.getWorldName()))
if (!lodWorld.worldName.equals(LodDimensionFileHandler.getWorldName()))
{
// this lodWorld isn't for this minecraft world
// delete it so we can get a new one
@@ -106,7 +106,6 @@ public class LodBuilder
// they will throw errors as they try to access things that no longer
// exist.
}
});
lodGenThreadPool.execute(thread);
@@ -119,9 +118,9 @@ public class LodBuilder
*/
public boolean isValidChunk(Chunk chunk)
{
ExtendedBlockStorage[] data = chunk.getBlockStorageArray();
ExtendedBlockStorage[] blockStorage = chunk.getBlockStorageArray();
for(ExtendedBlockStorage e : data)
for(ExtendedBlockStorage e : blockStorage)
{
if(e != null && !e.isEmpty())
{
@@ -23,9 +23,9 @@ import net.minecraft.world.storage.ISaveHandler;
* @author James Seibel
* @version 01-30-2021
*/
public class LodFileHandler
public class LodDimensionFileHandler
{
private LodDimension loadedRegion = null;
private LodDimension loadedDimension = null;
public long regionLastWriteTime[][];
// String s = Minecraft.getMinecraftDir().getCanonicalPath() + "/saves/" + world.getSaveHandler().getSaveDirectoryName() + "/data/AA/World" + world.provider.dimensionId + ".dat";
@@ -36,19 +36,17 @@ public class LodFileHandler
private final String FILE_EXTENSION = ".txt";
private ExecutorService fileWritingThreadPool = Executors.newFixedThreadPool(1);
/** Is true if the readyToReadAndWrite is false */
private boolean waitingToSaveRegions = false;
public LodFileHandler(ISaveHandler newSaveHandler, LodDimension newLoadedRegion)
public LodDimensionFileHandler(ISaveHandler newSaveHandler, LodDimension newLoadedDimension)
{
saveHandler = newSaveHandler;
loadedRegion = newLoadedRegion;
loadedDimension = newLoadedDimension;
// these two variable are used in sync with the LodDimension
regionLastWriteTime = new long[loadedRegion.getWidth()][loadedRegion.getWidth()];
for(int i = 0; i < loadedRegion.getWidth(); i++)
for(int j = 0; j < loadedRegion.getWidth(); j++)
regionLastWriteTime = new long[loadedDimension.getWidth()][loadedDimension.getWidth()];
for(int i = 0; i < loadedDimension.getWidth(); i++)
for(int j = 0; j < loadedDimension.getWidth(); j++)
regionLastWriteTime[i][j] = -1;
if (saveHandler != null && saveHandler.getWorldDirectory() != null)
@@ -141,8 +139,10 @@ public class LodFileHandler
// Save to File //
//==============//
public synchronized void saveDirtyRegionsToFile()
/**
* Save all dirty regions in this LodDimension to file.
*/
public synchronized void saveDirtyRegionsToFileAsync()
{
// we don't currently support reading or writing
// files when connected to a server
@@ -150,64 +150,31 @@ public class LodFileHandler
return;
if (!readyToReadAndWrite())
{
// we aren't ready to read and write yet
if(!waitingToSaveRegions)
{
waitingToSaveRegions = true;
// retry until we are able to read and write
// then wake up the fileWritingThreadPool
Thread retryReady = new Thread(() ->
{
try
{
// check once every so often so see
// if anything has changed so we can
// start reading and writing files
while(!readyToReadAndWrite())
{
this.wait(1000);
// get the save handler again, if for some
// reason the original handler was null
saveHandler = Minecraft.getMinecraft().getIntegratedServer().getWorld(0).getSaveHandler();
save_dir = getWorldSaveDirectory();
}
// we can start writing files now
fileWritingThreadPool.execute(saveDirtyRegionsThread);
waitingToSaveRegions = false;
}
catch (InterruptedException e)
{ /* should never be called */}
});
retryReady.run();
}
return;
}
fileWritingThreadPool.execute(saveDirtyRegionsThread);
}
private Thread saveDirtyRegionsThread = new Thread(() ->
{
for(int i = 0; i < loadedRegion.getWidth(); i++)
for(int i = 0; i < loadedDimension.getWidth(); i++)
{
for(int j = 0; j < loadedRegion.getWidth(); j++)
for(int j = 0; j < loadedDimension.getWidth(); j++)
{
if(loadedRegion.isRegionDirty[i][j])
if(loadedDimension.isRegionDirty[i][j])
{
saveRegionToDisk(loadedRegion.regions[i][j]);
loadedRegion.isRegionDirty[i][j] = false;
saveRegionToDisk(loadedDimension.regions[i][j]);
loadedDimension.isRegionDirty[i][j] = false;
}
}
}
waitingToSaveRegions = false;
});
/**
* Save a specific region to disk.<br>
* Note: it will save to the LodDimension that this
* handler is associated with.
*/
private void saveRegionToDisk(LodRegion region)
{
if (!readyToReadAndWrite() || region == null)
@@ -258,15 +225,13 @@ public class LodFileHandler
* Return the name of the file that should contain the
* region at the given x and z. <br>
* Returns null if this object isn't ready to read and write.
* @param regionX
* @param regionZ
*/
private String getFileNameForRegion(int regionX, int regionZ)
{
if (!readyToReadAndWrite())
return null;
return save_dir + "\\lod_data\\DIM" + loadedRegion.dimension.getId() + "\\" +
return save_dir + "\\lod_data\\DIM" + loadedDimension.dimension.getId() + "\\" +
FILE_NAME_PREFIX + "." + regionX + "." + regionZ + FILE_EXTENSION;
}
@@ -274,6 +239,8 @@ public class LodFileHandler
/**
* Returns if this FileHandler is ready to read
* and write files.
* <br>
* This returns true when the world save directory is known.
*/
public boolean readyToReadAndWrite()
{
@@ -306,6 +273,8 @@ public class LodFileHandler
/**
* Gets the canonical path to the world save folder.
* <br>
* Returns null if there was an IO Exception
*/
private String getWorldSaveDirectory()
@@ -11,7 +11,8 @@ import net.minecraft.client.Minecraft;
/**
* This object is used to get variables from methods
* where they are private.
* where they are private. Specifically the fog setting
* in Optifine.
*
* @author James Seibel
* @version 09-21-2020
@@ -3,7 +3,7 @@ package com.backsun.lod.objects;
import java.awt.Color;
import com.backsun.lod.util.enums.ColorDirection;
import com.backsun.lod.util.enums.LodLocation;
import com.backsun.lod.util.enums.LodCorner;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
@@ -73,7 +73,7 @@ public class LodChunk
//==============//
/**
* Create an empty LodChunk
* Create an empty invisible LodChunk at (0,0)
*/
public LodChunk()
{
@@ -145,7 +145,7 @@ public class LodChunk
// top
top = new short[4];
for(LodLocation loc : LodLocation.values())
for(LodCorner loc : LodCorner.values())
{
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex + 1);
@@ -156,7 +156,7 @@ public class LodChunk
// bottom
bottom = new short[4];
for(LodLocation loc : LodLocation.values())
for(LodCorner loc : LodCorner.values())
{
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex + 1);
@@ -202,11 +202,11 @@ public class LodChunk
}
/**
* Illegal argument is thrown if either the
* chunk or world is null. The reason the world
* can't be null is because it's required to determine
* a block's color.
* @throws IllegalArgumentException
* Creates a LodChunk for a chunk in the given world. <br>
* Note: The world is required to determine each block's color
*
* @throws IllegalArgumentException
* thrown if either the chunk or world is null.
*/
public LodChunk(Chunk chunk, World world) throws IllegalArgumentException
{
@@ -228,16 +228,16 @@ public class LodChunk
colors = new Color[6];
// generate the top and bottom points of this LOD
for(LodLocation loc : LodLocation.values())
for(LodCorner loc : LodCorner.values())
{
top[loc.value] = generateLodSection(chunk, true, loc);
bottom[loc.value] = generateLodSection(chunk, false, loc);
top[loc.value] = generateLodCorner(chunk, SectionGenerationMode.GENERATE_TOP, loc);
bottom[loc.value] = generateLodCorner(chunk, SectionGenerationMode.GENERATE_BOTTOM, loc);
}
// determine the average color for each direction
for(ColorDirection dir : ColorDirection.values())
{
colors[dir.value] = generateLodColorSection(chunk, world, dir);
colors[dir.value] = generateLodColor(chunk, world, dir);
}
}
@@ -253,15 +253,17 @@ public class LodChunk
/**
* Generate the height for the given LodLocation, either the top or bottom.
* <br><br>
* If invalid/null/empty chunks are given
* crashes may occur.
*/
public short generateLodSection(Chunk chunk, boolean getTopSection, LodLocation lodLoc)
private short generateLodCorner(Chunk chunk, SectionGenerationMode generationMode, LodCorner lodLoc)
{
// should have a length of 16
// (each storage is 16x16x16 and the
// world height is 256)
ExtendedBlockStorage[] data = chunk.getBlockStorageArray();
ExtendedBlockStorage[] blockStorage = chunk.getBlockStorageArray();
@@ -313,21 +315,29 @@ public class LodChunk
}
if(getTopSection)
return determineTopPoint(data, startX, endX, startZ, endZ);
if(generationMode == SectionGenerationMode.GENERATE_TOP)
return determineTopPoint(blockStorage, startX, endX, startZ, endZ);
else
return determineBottomPoint(data, startX, endX, startZ, endZ);
return determineBottomPoint(blockStorage, startX, endX, startZ, endZ);
}
/** GENERATE_TOP, GENERATE_BOTTOM */
private enum SectionGenerationMode
{
GENERATE_TOP,
GENERATE_BOTTOM;
}
private short determineBottomPoint(ExtendedBlockStorage[] data, int startX, int endX, int startZ, int endZ)
/**
* Find the lowest valid point from the bottom.
*/
private short determineBottomPoint(ExtendedBlockStorage[] blockStorage, int startX, int endX, int startZ, int endZ)
{
// search from the bottom up
for(int i = 0; i < data.length; i++)
for(int i = 0; i < blockStorage.length; i++)
{
for(int y = 0; y < CHUNK_DATA_HEIGHT; y++)
{
if(isLayerValidLodPoint(data, startX, endX, startZ, endZ, i, y))
if(isLayerValidLodPoint(blockStorage, startX, endX, startZ, endZ, i, y))
{
// we found
// enough blocks in this
@@ -335,23 +345,24 @@ public class LodChunk
// LOD point
return (short) (y + (i * CHUNK_DATA_HEIGHT));
}
} // y
} // data
}
}
// we never found a valid LOD point
return -1;
}
private short determineTopPoint(ExtendedBlockStorage[] data, int startX, int endX, int startZ, int endZ)
/**
* Find the highest valid point from the Top
*/
private short determineTopPoint(ExtendedBlockStorage[] blockStorage, int startX, int endX, int startZ, int endZ)
{
// search from the top down
for(int i = data.length - 1; i >= 0; i--)
for(int i = blockStorage.length - 1; i >= 0; i--)
{
for(int y = CHUNK_DATA_WIDTH - 1; y >= 0; y--)
{
if(isLayerValidLodPoint(data, startX, endX, startZ, endZ, i, y))
if(isLayerValidLodPoint(blockStorage, startX, endX, startZ, endZ, i, y))
{
// we found
// enough blocks in this
@@ -359,10 +370,8 @@ public class LodChunk
// LOD point
return (short) (y + (i * CHUNK_DATA_HEIGHT));
}
} // y
} // data
}
}
// we never found a valid LOD point
return -1;
@@ -373,7 +382,7 @@ public class LodChunk
* values a valid LOD point?
*/
private boolean isLayerValidLodPoint(
ExtendedBlockStorage[] data,
ExtendedBlockStorage[] blockStorage,
int startX, int endX,
int startZ, int endZ,
int dataIndex, int y)
@@ -385,7 +394,7 @@ public class LodChunk
{
for(int z = startZ; z < endZ; z++)
{
if(data[dataIndex] == null)
if(blockStorage[dataIndex] == null)
{
// this section doesn't have any blocks,
// it is not a valid section
@@ -393,7 +402,7 @@ public class LodChunk
}
else
{
if(data[dataIndex].get(x, y, z) != null && Block.getIdFromBlock(data[dataIndex].get(x, y, z).getBlock()) != airBlockId)
if(blockStorage[dataIndex].get(x, y, z) != null && Block.getIdFromBlock(blockStorage[dataIndex].get(x, y, z).getBlock()) != airBlockId)
{
// we found a valid block in
// in this layer
@@ -412,9 +421,11 @@ public class LodChunk
return false;
}
private Color generateLodColorSection(Chunk chunk, World world, ColorDirection colorDir)
/**
* Generate the color of the given ColorDirection at the given chunk
* in the given world.
*/
private Color generateLodColor(Chunk chunk, World world, ColorDirection colorDir)
{
Minecraft mc = Minecraft.getMinecraft();
BlockColors bc = mc.getBlockColors();
@@ -441,11 +452,18 @@ public class LodChunk
}
/**
* Only accepts TOP and BOTTOM as ColorPositions
* Generates the color of the top or bottom of a given chunk in the given world.
*
* @throws IllegalArgumentException if given a ColorDirection other than TOP or BOTTOM
*/
private Color generateLodColorVertical(Chunk chunk, ColorDirection colorDir, World world, BlockColors bc)
{
ExtendedBlockStorage[] data = chunk.getBlockStorageArray();
if(colorDir != ColorDirection.TOP && colorDir != ColorDirection.BOTTOM)
{
throw new IllegalArgumentException("generateLodColorVertical only accepts the ColorDirection TOP or BOTTOM");
}
ExtendedBlockStorage[] blockStorage = chunk.getBlockStorageArray();
int numbOfBlocks = 0;
int red = 0;
@@ -456,8 +474,8 @@ public class LodChunk
// either go top down or bottom up
int dataStart = goTopDown? data.length - 1 : 0;
int dataMax = data.length;
int dataStart = goTopDown? blockStorage.length - 1 : 0;
int dataMax = blockStorage.length;
int dataMin = 0;
int dataIncrement = goTopDown? -1 : 1;
@@ -474,16 +492,16 @@ public class LodChunk
for(int di = dataStart; !foundBlock && di >= dataMin && di < dataMax; di += dataIncrement)
{
if(!foundBlock && data[di] != null)
if(!foundBlock && blockStorage[di] != null)
{
for(int y = topStart; !foundBlock && y >= topMin && y < topMax; y += topIncrement)
{
int ci;
if(Block.getIdFromBlock(data[di].get(x, y, z).getBlock()) == waterBlockId)
if(Block.getIdFromBlock(blockStorage[di].get(x, y, z).getBlock()) == waterBlockId)
// this is a special case since getColor on water generally returns white
ci = waterColor;
else
ci = bc.getColor(data[di].get(x, y, z), world, new BlockPos(x,y,z));
ci = bc.getColor(blockStorage[di].get(x, y, z), world, new BlockPos(x,y,z));
if(ci == 0)
{
@@ -519,10 +537,20 @@ public class LodChunk
return new Color(red, green, blue);
}
/**
* Generates the color of the side of a given chunk in the given world for the given ColorDirection.
*
* @throws IllegalArgumentException if given a ColorDirection other than N, S, W, E (North, South, East, West)
*/
private Color generateLodColorHorizontal(Chunk chunk, ColorDirection colorDir, World world, BlockColors bc)
{
ExtendedBlockStorage[] data = chunk.getBlockStorageArray();
if(colorDir != ColorDirection.N && colorDir != ColorDirection.S && colorDir != ColorDirection.E && colorDir != ColorDirection.W)
{
throw new IllegalArgumentException("generateLodColorHorizontal only accepts the ColorDirection N (North), S (South), E (East), or W (West)");
}
ExtendedBlockStorage[] blockStorage = chunk.getBlockStorageArray();
int numbOfBlocks = 0;
int red = 0;
@@ -563,9 +591,9 @@ public class LodChunk
}
for (int di = 0; di < data.length; di++)
for (int di = 0; di < blockStorage.length; di++)
{
if (data[di] != null)
if (blockStorage[di] != null)
{
for (int y = 0; y < CHUNK_DATA_HEIGHT; y++)
{
@@ -607,11 +635,11 @@ public class LodChunk
}
int ci;
if(Block.getIdFromBlock(data[di].get(x, y, z).getBlock()) == waterBlockId)
if(Block.getIdFromBlock(blockStorage[di].get(x, y, z).getBlock()) == waterBlockId)
// this is a special case since getColor on water generally returns white
ci = waterColor;
else
ci = bc.getColor(data[di].get(x, y, z), world, new BlockPos(x,y,z));
ci = bc.getColor(blockStorage[di].get(x, y, z), world, new BlockPos(x,y,z));
if (ci == 0) {
// skip air or invisible blocks
@@ -671,6 +699,30 @@ public class LodChunk
//================//
// misc functions //
//================//
/**
* If this LOD is either invisible from every
* direction or doesn't have a valid height
* it is empty.
*/
public boolean isLodEmpty()
{
for(LodCorner corner : LodCorner.values())
if(top[corner.value] != -1 || bottom[corner.value] != -1)
// at least one corner is valid
return false;
Color invisible = new Color(0,0,0,0);
for(ColorDirection dir : ColorDirection.values())
if(!colors[dir.value].equals(invisible))
// at least one direction has a non-invisible color
return false;
return true;
}
@@ -681,7 +733,6 @@ public class LodChunk
//========//
/**
* Outputs all data in csv format
* with the given delimiter.
@@ -727,27 +778,6 @@ public class LodChunk
s += "x: " + x + " z: " + z + "\t";
// s += "top: ";
// for(int i = 0; i < top.length; i++)
// {
// s += top[i] + " ";
// }
// s += "\t";
// s += "bottom: ";
// for(int i = 0; i < bottom.length; i++)
// {
// s += bottom[i] + " ";
// }
// s += "\t";
// s += "colors ";
// for(int i = 0; i < colors.length; i++)
// {
// if(colors[i] != null)
// s += "(" + colors[i].getRed() + ", " + colors[i].getGreen() + ", " + colors[i].getBlue() + "), ";
// }
s += "(" + colors[ColorDirection.TOP.value].getRed() + ", " + colors[ColorDirection.TOP.value].getGreen() + ", " + colors[ColorDirection.TOP.value].getBlue() + "), ";
return s;
@@ -1,6 +1,6 @@
package com.backsun.lod.objects;
import com.backsun.lod.handlers.LodFileHandler;
import com.backsun.lod.handlers.LodDimensionFileHandler;
import net.minecraft.client.Minecraft;
import net.minecraft.world.DimensionType;
@@ -10,13 +10,13 @@ import net.minecraft.world.DimensionType;
* for a given dimension.
*
* @author James Seibel
* @version 01-31-2021
* @version 02-23-2021
*/
public class LodDimension
{
public final DimensionType dimension;
private volatile int width; // if this ever changes make sure to update the halfWidth too
private volatile int width;
private volatile int halfWidth;
public LodRegion regions[][];
@@ -25,7 +25,8 @@ public class LodDimension
private int centerX;
private int centerZ;
private LodFileHandler rfHandler;
private LodDimensionFileHandler fileHandler;
public LodDimension(DimensionType newDimension, int newMaxWidth)
{
@@ -33,7 +34,7 @@ public class LodDimension
width = newMaxWidth;
// dimension 0 works here since we are just looking for the save handler anyway
rfHandler = new LodFileHandler(Minecraft.getMinecraft().getIntegratedServer().getWorld(0).getSaveHandler(), this);
fileHandler = new LodDimensionFileHandler(Minecraft.getMinecraft().getIntegratedServer().getWorld(0).getSaveHandler(), this);
regions = new LodRegion[width][width];
isRegionDirty = new boolean[width][width];
@@ -50,7 +51,10 @@ public class LodDimension
}
/**
* Move the center of this LodDimension and move all owned
* regions over by the given x and z offset.
*/
public void move(int xOffset, int zOffset)
{
// if the x or z offset is equal to or greater than
@@ -144,22 +148,16 @@ public class LodDimension
}
public int getCenterX()
{
return centerX;
}
public int getCenterZ()
{
return centerZ;
}
/**
* Gets the region at the given X and Z
* <br>
* Returns null if the region doesn't exist
* or is outside the loaded area.
*/
public LodRegion getRegion(int regionX, int regionZ)
{
int xIndex = (regionX - centerX) + halfWidth;
@@ -201,7 +199,11 @@ public class LodDimension
/**
* Add the given LOD to this dimension at the coordinate
* stored in the LOD. If an LOD already exists at the given
* coordinates it will be overwritten.
*/
public void addLod(LodChunk lod)
{
int regionX = (lod.x + centerX) / LodRegion.SIZE;
@@ -236,14 +238,15 @@ public class LodDimension
int xIndex = (regionX - centerX) + halfWidth;
int zIndex = (regionZ - centerZ) + halfWidth;
isRegionDirty[xIndex][zIndex] = true;
rfHandler.saveDirtyRegionsToFile();
fileHandler.saveDirtyRegionsToFileAsync();
}
/**
* Returns null if the LodChunk isn't loaded
* Get the LodChunk at the given X and Z coordinates
* in this dimension.
* <br>
* Returns null if the LodChunk doesn't exist or
* is outside the loaded area.
*/
public LodChunk getLodFromCoordinates(int chunkX, int chunkZ)
{
@@ -271,11 +274,13 @@ public class LodDimension
}
/**
* Get the region at the given X and Z coordinates from the
* RegionFileHandler.
*/
public LodRegion getRegionFromFile(int regionX, int regionZ)
{
return rfHandler.loadRegionFromFile(regionX, regionZ);
return fileHandler.loadRegionFromFile(regionX, regionZ);
}
@@ -293,6 +298,22 @@ public class LodDimension
public int getCenterX()
{
return centerX;
}
public int getCenterZ()
{
return centerZ;
}
public int getWidth()
{
return width;
@@ -311,6 +332,18 @@ public class LodDimension
for(int j = 0; j < width; j++)
isRegionDirty[i][j] = false;
}
@Override
public String toString()
{
String s = "";
s += "dim: " + dimension.getName() + "\t";
s += "(" + centerX + "," + centerZ + ")";
return s;
}
}
@@ -7,7 +7,7 @@ package com.backsun.lod.objects;
* one file in the file system.
*
* @author James Seibel
* @version 1-20-2021
* @version 1-22-2021
*/
public class LodRegion
{
@@ -31,6 +31,11 @@ public class LodRegion
}
/**
* Add the given LOD to this region at the coordinate
* stored in the LOD. If an LOD already exists at the given
* coordinates it will be overwritten.
*/
public void addLod(LodChunk lod)
{
// we use ABS since LODs can be negative, but if they are
@@ -43,6 +48,13 @@ public class LodRegion
chunks[xIndex][zIndex] = lod;
}
/**
* Get the LodChunk at the given X and Z coordinates
* in this region.
* <br>
* Returns null if the LodChunk doesn't exist or
* is outside the loaded area.
*/
public LodChunk getLod(int chunkX, int chunkZ)
{
// since we add LOD's with ABS, we get them the same way
@@ -52,11 +64,16 @@ public class LodRegion
if(arrayX >= SIZE || arrayZ >= SIZE)
return null;
// TODO fix some LOD strips showing up in the wrong location
// issue #2
// maybe this has to do with ABS being used incorrectly?
return chunks[arrayX][arrayZ];
}
/**
* Returns all LodChunks in this region
*/
public LodChunk[][] getAllLods()
{
return chunks;
@@ -1,14 +1,13 @@
package com.backsun.lod.objects;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;
/**
* This stores all LODs for a given world.
*
* @author James Seibel
* @version 01-31-2021
* @version 02-22-2021
*/
public class LodWorld
{
@@ -17,7 +16,7 @@ public class LodWorld
/**
* Key = Dimension id (as an int)
*/
private Dictionary<Integer, LodDimension> lodDimensions;
private Map<Integer, LodDimension> lodDimensions;
public LodWorld(String newWorldName)
@@ -38,12 +37,27 @@ public class LodWorld
return lodDimensions.get(dimensionId);
}
/**
* Resizes the max width in regions that each LodDimension
* should use.
*/
public void resizeDimensionRegionWidth(int newWidth)
{
Enumeration<Integer> keys = lodDimensions.keys();
for(Integer key : lodDimensions.keySet())
lodDimensions.get(key).setRegionWidth(newWidth);
}
@Override
public String toString()
{
String s = "";
while(keys.hasMoreElements())
lodDimensions.get(keys.nextElement()).setRegionWidth(newWidth);
s += worldName + "\t - dimensions: ";
for(Integer key : lodDimensions.keySet())
s += lodDimensions.get(key).dimension.getName() + ", ";
return s;
}
}
@@ -22,10 +22,11 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
// Minecraft.getMinecraft().getIntegratedServer()
/**
* This is used by the client.
* This handles all events sent to the client,
* and is the starting point for most of this program.
*
* @author James_Seibel
* @version 02-21-2021
* @version 02-23-2021
*/
public class ClientProxy extends CommonProxy
{
@@ -57,6 +58,10 @@ public class ClientProxy extends CommonProxy
GL11.glDisable(GL11.GL_STENCIL_TEST);
}
/**
* Do any setup that is required to draw LODs
* and then tell the LodRenderer to draw.
*/
public void renderLods(float partialTicks)
{
int newWidth = Math.max(4, (Minecraft.getMinecraft().gameSettings.renderDistanceChunks * LodChunk.WIDTH * 2) / LodRegion.SIZE);
@@ -116,7 +121,7 @@ public class ClientProxy extends CommonProxy
@SubscribeEvent
public void chunkLoadEvent(ChunkEvent event)
{
lodWorld = lodBuilder.generateLodChunk(event.getChunk());
lodWorld = lodBuilder.generateLodChunkAsync(event.getChunk());
}
/**
@@ -132,7 +137,7 @@ public class ClientProxy extends CommonProxy
if(world != null)
{
lodWorld = lodBuilder.generateLodChunk(world.getChunkFromChunkCoords(event.getChunkX(), event.getChunkZ()));
lodWorld = lodBuilder.generateLodChunkAsync(world.getChunkFromChunkCoords(event.getChunkX(), event.getChunkZ()));
}
}
}
@@ -1,7 +1,7 @@
package com.backsun.lod.proxy;
/**
* This is used by the server.
* This handles any events sent to the server.
*
* @author James_Seibel
* @version 08-31-2020
@@ -24,7 +24,7 @@ import com.backsun.lod.util.LodConfig;
import com.backsun.lod.util.enums.ColorDirection;
import com.backsun.lod.util.enums.FogDistance;
import com.backsun.lod.util.enums.FogQuality;
import com.backsun.lod.util.enums.LodLocation;
import com.backsun.lod.util.enums.LodCorner;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BufferBuilder;
@@ -41,6 +41,10 @@ import net.minecraft.util.math.MathHelper;
*/
public class LodRenderer
{
/** this is the light used when rendering the LODs,
* it should be something different than what is used by Minecraft */
private static final int LOD_GL_LIGHT_NUMBER = GL11.GL_LIGHT2;
/** If true the LODs colors will be replaced with
* a checkerboard, this can be used for debugging. */
public boolean debugging = false;
@@ -98,23 +102,22 @@ public class LodRenderer
mc = Minecraft.getMinecraft();
// for some reason "Tessellator.getInstance()" won't work here, we have to create a new one
tessellator = new Tessellator(2097152);
tessellator = new Tessellator(2097152); // the number here is what is used by the default Tessellator
bufferBuilder = tessellator.getBuffer();
reflectionHandler = new ReflectionHandler();
}
/**
* Besides drawing the LODs this method also starts
* the async process of generating the Buffers that hold those LODs.
*
* @param newDimension The dimension to draw, if null doesn't replace the current dimension.
* @param partialTicks how far into the current tick this method was called.
*/
public void drawLODs(LodDimension newDimension, float partialTicks)
{
if (reflectionHandler.fovMethod == null)
{
// don't continue if we can't get the
// user's FOV
return;
}
if (reflectionHandler.fovMethod == null)
{
// we aren't able to get the user's
@@ -131,6 +134,19 @@ public class LodRenderer
//===============//
// initial setup //
//===============//
// used for debugging and viewing how long different processes take
mc.mcProfiler.endSection();
mc.mcProfiler.startSection("LOD");
mc.mcProfiler.startSection("LOD setup");
// should LODs be regenerated?
if ((int)Minecraft.getMinecraft().player.posX / LodChunk.WIDTH != prevChunkX ||
(int)Minecraft.getMinecraft().player.posZ / LodChunk.WIDTH != prevChunkZ ||
@@ -138,6 +154,7 @@ public class LodRenderer
prevFogDistance != LodConfig.fogDistance ||
lodDimension != newDimension)
{
// yes
regen = true;
prevChunkX = (int)Minecraft.getMinecraft().player.posX / LodChunk.WIDTH;
@@ -152,17 +169,9 @@ public class LodRenderer
regen = false;
}
lodDimension = newDimension;
if (newDimension != null)
lodDimension = newDimension;
// used for debugging and viewing how long different processes take
mc.mcProfiler.endSection();
mc.mcProfiler.startSection("LOD");
mc.mcProfiler.startSection("LOD setup");
if (LodConfig.drawCheckerBoard)
{
if (debugging != LodConfig.drawCheckerBoard)
@@ -183,9 +192,6 @@ public class LodRenderer
double cameraX = player.lastTickPosX + (player.posX - player.lastTickPosX) * partialTicks;
double cameraY = player.lastTickPosY + (player.posY - player.lastTickPosY) * partialTicks;
double cameraZ = player.lastTickPosZ + (player.posZ - player.lastTickPosZ) * partialTicks;
// determine how far the game's render distance is currently set
int renderDistWidth = mc.gameSettings.renderDistanceChunks;
@@ -204,29 +210,32 @@ public class LodRenderer
// create the LODs //
//=================//
// only regenerate LODs if:
// only regenerate the LODs if:
// 1. we want to regenerate LODs
// 2. we aren't already regenerating LODs
// 2. we aren't already regenerating the LODs
// 3. we aren't waiting for the build and draw buffers to swap
// (this is to prevent thread conflicts)
if (regen && !regenerating && !switchBuffers)
{
mc.mcProfiler.endStartSection("LOD generation");
regenerating = true;
// this will only be called once, unless the numbBufferThreads changes
if (numbBufferThreads != bufferThreads.size())
setupBufferThreads();
// this will mainly happen when the view distance is changed
if (drawableNearBuffers == null || drawableFarBuffers == null ||
previousChunkRenderDistance != mc.gameSettings.renderDistanceChunks)
setupBuffers(numbChunksWide);
// generate the LODs on a separate thread to prevent stuttering or freezing
genThread.execute(createLodBufferGenerationThread(cameraX, cameraZ, numbChunksWide));
}
// replace the buffers used to draw and build,
// this is done to keep everything thread safe
// this is only done when the createLodBufferGenerationThread
// has finished executing on a parallel thread.
if (switchBuffers)
{
swapBuffers();
@@ -242,13 +251,13 @@ public class LodRenderer
//===========================//
// set the required open GL settings
GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glLineWidth(2.0f);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glEnable(GL11.GL_CULL_FACE);
GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
// move the LODs so they are in the correct place relative to the camera
GlStateManager.translate(-cameraX, -cameraY, -cameraZ);
setProjectionMatrix(partialTicks);
@@ -266,21 +275,26 @@ public class LodRenderer
switch(LodConfig.fogDistance)
{
case NEAR_AND_FAR:
mc.mcProfiler.endStartSection("LOD draw setup");
// when drawing NEAR_AND_FAR fog we need 2 draw
// calls since fog can only go in one direction at a time
mc.mcProfiler.endStartSection("LOD draw");
setupFog(FogDistance.NEAR, reflectionHandler.getFogQuality());
sendLodsToGpuAndDraw(drawableNearBuffers);
mc.mcProfiler.endStartSection("LOD draw setup");
mc.mcProfiler.endStartSection("LOD draw");
setupFog(FogDistance.FAR, reflectionHandler.getFogQuality());
sendLodsToGpuAndDraw(drawableFarBuffers);
break;
case NEAR:
mc.mcProfiler.endStartSection("LOD draw setup");
mc.mcProfiler.endStartSection("LOD draw");
setupFog(FogDistance.NEAR, reflectionHandler.getFogQuality());
sendLodsToGpuAndDraw(drawableNearBuffers);
break;
case FAR:
mc.mcProfiler.endStartSection("LOD draw setup");
mc.mcProfiler.endStartSection("LOD draw");
setupFog(FogDistance.FAR, reflectionHandler.getFogQuality());
sendLodsToGpuAndDraw(drawableFarBuffers);
break;
@@ -302,7 +316,7 @@ public class LodRenderer
GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glDisable(GL11.GL_LIGHT2);
GL11.glDisable(LOD_GL_LIGHT_NUMBER);
GL11.glDisable(GL11.GL_COLOR_MATERIAL);
// change the perspective matrix back to prevent incompatibilities
@@ -319,49 +333,11 @@ public class LodRenderer
}
/**
* draw an array of cubes (or squares) with the given colors.
* @param lods bounding boxes to draw
* @param colors color of each box to draw
* This is where the actual drawing happens.
*
* @param buffers the buffers sent to the GPU to draw
*/
private void generateLodBuffers(AxisAlignedBB[][] lods, Color[][] colors, FogDistance fogDistance)
{
List<Future<NearFarBuffer>> bufferFutures = new ArrayList<>();
for(int i = 0; i < numbBufferThreads; i++)
{
bufferThreads.get(i).setNewData(buildableNearBuffers[i], buildableFarBuffers[i], fogDistance, lods, colors, i, numbBufferThreads);
}
try
{
bufferFutures = bufferThreadPool.invokeAll(bufferThreads);
}
catch (InterruptedException e)
{
// this should never happen, but just in case
e.printStackTrace();
}
for(int i = 0; i < numbBufferThreads; i++)
{
try
{
buildableNearBuffers[i] = bufferFutures.get(i).get().nearBuffer;
buildableFarBuffers[i] = bufferFutures.get(i).get().farBuffer;
}
catch(CancellationException | ExecutionException| InterruptedException e)
{
// this should never happen, but just in case
e.printStackTrace();
}
}
}
private void sendLodsToGpuAndDraw(BufferBuilder[] buffers)
{
for(int i = 0; i < numbBufferThreads; i++)
@@ -371,11 +347,10 @@ public class LodRenderer
bufferBuilder.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_COLOR);
bufferBuilder.getByteBuffer().clear();
// replace the data in bufferBuilder with the data from the given buffer
bufferBuilder.putBulkData(buffers[i].getByteBuffer());
mc.mcProfiler.endStartSection("LOD draw");
tessellator.draw();
mc.mcProfiler.endStartSection("LOD draw setup");
bufferBuilder.getByteBuffer().clear(); // this is required otherwise nothing is drawn
}
@@ -451,7 +426,6 @@ public class LodRenderer
/**
* create a new projection matrix and send it over to the GPU
* @param partialTicks how many ticks into the frame we are
* @return true if the matrix was successfully created and sent to the GPU, false otherwise
*/
private void setProjectionMatrix(float partialTicks)
{
@@ -483,16 +457,18 @@ public class LodRenderer
float gammaMultiplyer = (mc.gameSettings.gammaSetting * 0.5f + 0.5f);
float lightStrength = sunBrightness * skyHasLight * gammaMultiplyer;
float lightAmbient[] = {lightStrength, lightStrength, lightStrength, 1.0f};
ByteBuffer temp = ByteBuffer.allocateDirect(16);
temp.order(ByteOrder.nativeOrder());
GL11.glLight(GL11.GL_LIGHT2, GL11.GL_AMBIENT, (FloatBuffer) temp.asFloatBuffer().put(lightAmbient).flip());
GL11.glEnable(GL11.GL_LIGHT2); // Enable the above lighting
GL11.glLight(LOD_GL_LIGHT_NUMBER, GL11.GL_AMBIENT, (FloatBuffer) temp.asFloatBuffer().put(lightAmbient).flip());
GL11.glEnable(LOD_GL_LIGHT_NUMBER); // Enable the above lighting
GlStateManager.enableLighting();
}
/**
* create the BuildBufferThreads
*/
private void setupBufferThreads()
{
bufferThreads.clear();
@@ -501,7 +477,7 @@ public class LodRenderer
}
/**
*
* Create all buffers that will be used.
*/
private void setupBuffers(int numbChunksWide)
{
@@ -540,17 +516,17 @@ public class LodRenderer
/**
* Returns -1 if there are no valid points
* @Returns -1 if there are no valid points
*/
private int getHighestPointInLod(short[] heightPoints)
private int getValidHeightPoint(short[] heightPoints)
{
if (heightPoints[LodLocation.NE.value] != -1)
return heightPoints[LodLocation.NE.value];
if (heightPoints[LodLocation.NW.value] != -1)
return heightPoints[LodLocation.NW.value];
if (heightPoints[LodLocation.SE.value] != -1)
return heightPoints[LodLocation.NE.value];
return heightPoints[LodLocation.NE.value];
if (heightPoints[LodCorner.NE.value] != -1)
return heightPoints[LodCorner.NE.value];
if (heightPoints[LodCorner.NW.value] != -1)
return heightPoints[LodCorner.NW.value];
if (heightPoints[LodCorner.SE.value] != -1)
return heightPoints[LodCorner.NE.value];
return heightPoints[LodCorner.NE.value];
}
@@ -603,7 +579,7 @@ public class LodRenderer
// this is just how chunk loading/unloading works. This can hopefully
// be hidden with careful use of fog)
int middle = (numbChunksWide / 2);
if (RenderUtil.isCoordinateInLoadedArea(i, j, middle))
if (isCoordInCenterArea(i, j, middle))
{
continue;
}
@@ -660,8 +636,8 @@ public class LodRenderer
// add the new box to the array
int topPoint = getHighestPointInLod(lod.top);
int bottomPoint = getHighestPointInLod(lod.bottom);
int topPoint = getValidHeightPoint(lod.top);
int bottomPoint = getValidHeightPoint(lod.bottom);
// don't draw an LOD if it is empty
if (topPoint == -1 && bottomPoint == -1)
@@ -679,6 +655,54 @@ public class LodRenderer
return t;
}
/**
* draw an array of boxes with the given colors.
* <br><br>
* Currently only one color per box is supported.
*
* @param lods bounding boxes to draw
* @param colors color of each box to draw
*/
private void generateLodBuffers(AxisAlignedBB[][] lods, Color[][] colors, FogDistance fogDistance)
{
List<Future<NearFarBuffer>> bufferFutures = new ArrayList<>();
// update the information that the bufferThreads are using
for(int i = 0; i < numbBufferThreads; i++)
{
bufferThreads.get(i).setNewData(buildableNearBuffers[i], buildableFarBuffers[i], fogDistance, lods, colors, i, numbBufferThreads);
}
// run all the bufferThreads and get their results
try
{
bufferFutures = bufferThreadPool.invokeAll(bufferThreads);
}
catch (InterruptedException e)
{
// this should never happen, but just in case
e.printStackTrace();
}
// update our buildable buffers
for(int i = 0; i < numbBufferThreads; i++)
{
try
{
buildableNearBuffers[i] = bufferFutures.get(i).get().nearBuffer;
buildableFarBuffers[i] = bufferFutures.get(i).get().farBuffer;
}
catch(CancellationException | ExecutionException| InterruptedException e)
{
// this should never happen, but just in case
e.printStackTrace();
}
}
}
/**
* Swap buildable and drawable buffers.
*/
@@ -705,4 +729,16 @@ public class LodRenderer
/**
* Returns if the given coordinate is in the loaded area of the world.
* @param centerCoordinate the center of the loaded world
*/
private boolean isCoordInCenterArea(int i, int j, int centerCoordinate)
{
return (i >= centerCoordinate - mc.gameSettings.renderDistanceChunks
&& i <= centerCoordinate + mc.gameSettings.renderDistanceChunks)
&&
(j >= centerCoordinate - mc.gameSettings.renderDistanceChunks
&& j <= centerCoordinate + mc.gameSettings.renderDistanceChunks);
}
}
@@ -1,45 +0,0 @@
package com.backsun.lod.renderer;
import net.minecraft.client.Minecraft;
/**
* This holds miscellaneous helper code
* to be used in the rendering process.
*
* @author James Seibel
* @version 2-13-2021
*/
public class RenderUtil
{
/**
* Returns if the given coordinate is in the loaded area of the world.
* @param centerCoordinate the center of the loaded world
*/
public static boolean isCoordinateInLoadedArea(int i, int j, int centerCoordinate)
{
Minecraft mc = Minecraft.getMinecraft();
return (i >= centerCoordinate - mc.gameSettings.renderDistanceChunks
&& i <= centerCoordinate + mc.gameSettings.renderDistanceChunks)
&&
(j >= centerCoordinate - mc.gameSettings.renderDistanceChunks
&& j <= centerCoordinate + mc.gameSettings.renderDistanceChunks);
}
/**
* Find the coordinates that are in the center half of the given
* 2D matrix, starting at (0,0) and going to (2 * lodRadius, 2 * lodRadius).
*/
public static boolean isCoordinateInNearFogArea(int i, int j, int lodRadius)
{
int halfRadius = lodRadius / 2;
return (i >= lodRadius - halfRadius
&& i <= lodRadius + halfRadius)
&&
(j >= lodRadius - halfRadius
&& j <= lodRadius + halfRadius);
}
}
@@ -9,6 +9,7 @@ import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
/**
* This is linked to Forge's mod config GUI.
*
* @author James Seibel
* @version 02-14-2021
@@ -1,6 +1,7 @@
package com.backsun.lod.util;
/**
* This holds meta information about the mod.
*
* @author James Seibel
* @version 04-16-2020
@@ -1,10 +1,10 @@
package com.backsun.lod.util.enums;
/**
* TOP, N, S, E, W, BOTTOM
*
* @author James Seibel
* @version 10-17-2020
*
* TOP, N, S, E, W, BOTTOM
*/
public enum ColorDirection
{
@@ -1,32 +0,0 @@
package com.backsun.lod.util.enums;
/**
* @author James Seibel
* @version 1-23-2021
*/
public enum DrawMode
{
/** Draw the LOD objects in groups.
* <br>
* <br>
* Fancy fog: render the center and outside LOD
* objects in 2 different groups.
* <br>
* Fast fog: render all LOD objects at one time.
*/
BATCH(0),
/** Draw each LOD objects separately.
* <br>
* <br>
* Not suggested normally since draw calls are GPU expensive.
*/
INDIVIDUAL(5);
public final int value;
private DrawMode(int newValue)
{
value = newValue;
}
}
@@ -8,9 +8,9 @@ package com.backsun.lod.util.enums;
*/
public enum FogDistance
{
/** valid for both fast and fancy fog qualities. */
/** good for fast or fancy fog qualities. */
NEAR,
/** valid for both fast and fancy fog qualities. */
/** good for fast or fancy fog qualities. */
FAR,
/** only looks good if the fog quality is set to Fancy. */
NEAR_AND_FAR;
@@ -1,13 +1,12 @@
package com.backsun.lod.util.enums;
/**
* NE, SE, SW, NW
*
* @author James Seibel
* @version 1-20-2020
*
* NE, SE, SW, NW
*/
public enum LodLocation
public enum LodCorner
{
// used for position
@@ -22,7 +21,7 @@ public enum LodLocation
public final int value;
private LodLocation(int newValue)
private LodCorner(int newValue)
{
value = newValue;
}
+1 -1
View File
@@ -3,7 +3,7 @@
"modid": "lod",
"name": "Level Of Details",
"description": "Generates and renders simplified chunks beyond the normal view distance, at a low performance cost.",
"version": "0.1",
"version": "1.0",
"mcversion": "1.12.2",
"url": "",
"updateUrl": "",