/*
* This file is part of Cubic Chunks Mod, licensed under the MIT License (MIT).
*
* Copyright (c) 2015 contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package cubicchunks.worldgen.generator.custom.structures;
import net.minecraft.block.state.IBlockState;
import net.minecraft.init.Blocks;
import net.minecraft.world.gen.structure.StructureBoundingBox;
import java.util.Random;
import java.util.function.Predicate;
import cubicchunks.util.CubePos;
import cubicchunks.util.StructureGenUtil;
import cubicchunks.world.ICubicWorld;
import cubicchunks.world.cube.Cube;
import cubicchunks.worldgen.generator.ICubePrimer;
import static cubicchunks.util.Coords.cubeToMinBlock;
import static cubicchunks.util.Coords.localToBlock;
import static cubicchunks.util.StructureGenUtil.normalizedDistance;
import static cubicchunks.util.StructureGenUtil.scanWallsForBlock;
import static java.lang.Math.max;
import static net.minecraft.util.math.MathHelper.cos;
import static net.minecraft.util.math.MathHelper.floor;
import static net.minecraft.util.math.MathHelper.sin;
/*
* Modified Minecraft cave generation code. Based on Robinton's cave generation implementation.
*/
//TODO: Fix code duplication beterrn cave and cave generators
public class CubicCaveGenerator extends CubicStructureGenerator {
//=============================================
//Possibly configurable values
//=============================================
/**
* 1 in CAVE_RARITY attempts will result in generating any caves at all
*/
private static final int CAVE_RARITY = 16*7;
/**
* Maximum amount of starting nodes
*/
private static final int MAX_INIT_NODES = 14;
/**
* 1 in LARGE_NODE_RARITY initial attempts will result in large node
*/
private static final int LARGE_NODE_RARITY = 4;
/**
* The maximum amount of additional branches after generating large node. Random value between 0 and
* LARGE_NODE_MAX_BRANCHES is chosen.
*/
private static final int LARGE_NODE_MAX_BRANCHES = 4;
/**
* 1 in BIG_CAVE_RARITY branches will start bigger than usual
*/
private static final int BIG_CAVE_RARITY = 10;
/**
* Value added to the size of the cave (radius)
*/
private static final double CAVE_SIZE_ADD = 1.5D;
/**
* In 1 of STEEP_STEP_RARITY steps, cave will be flattened using STEEPER_FLATTEN_FACTOR instead of FLATTEN_FACTOR
*/
private static final int STEEP_STEP_RARITY = 6;
/**
* After each step the Y direction component will be multiplied by this value, unless steeper cave is allowed
*/
private static final double FLATTEN_FACTOR = 0.7;
/**
* If steeper cave is allowed - this value will be used instead of FLATTEN_FACTOR
*/
private static final double STEEPER_FLATTEN_FACTOR = 0.92;
/**
* Each step cave direction angles will be changed by this fraction of values that specify how direction changes
*/
private static final double DIRECTION_CHANGE_FACTOR = 0.1;
/**
* This fraction of the previous value that controls horizontal direction changes will be used in next step
*/
private static final double PREV_HORIZ_DIRECTION_CHANGE_WEIGHT = 0.75;
/**
* This fraction of the previous value that controls vertical direction changes will be used in next step
*/
private static final double PREV_VERT_DIRECTION_CHANGE_WEIGHT = 0.9;
/**
* Maximum value by which horizontal cave direction randomly changes each step, lower values are much more likely.
*/
private static final double MAX_ADD_DIRECTION_CHANGE_HORIZ = 4.0;
/**
* Maximum value by which vertical cave direction randomly changes each step, lower values are much more likely.
*/
private static final double MAX_ADD_DIRECTION_CHANGE_VERT = 2.0;
/**
* 1 in this amount of steps will actually carve any blocks,
*/
private static final int CARVE_STEP_RARITY = 4;
/**
* Relative "height" if depth floor
* <p>
* -1 results in round cave without flat floor 1 will completely fill the cave 0 will result in lower half of the
* cave to be filled with stone
*/
private static final double CAVE_FLOOR_DEPTH = -0.7;
/**
* Controls which blocks can be replaced by cave
*/
private static final Predicate<IBlockState> isBlockReplaceable = (state ->
state.getBlock() == Blocks.STONE || state.getBlock() == Blocks.DIRT || state.getBlock() == Blocks.GRASS);
@Override
protected void generate(ICubicWorld world, ICubePrimer cube,
int cubeXOrigin, int cubeYOrigin, int cubeZOrigin, CubePos generatedCubePos) {
if (this.rand.nextInt(CAVE_RARITY) != 0) {
return;
}
//very low probability of generating high number
int nodes = this.rand.nextInt(this.rand.nextInt(this.rand.nextInt(MAX_INIT_NODES + 1) + 1) + 1);
for (int node = 0; node < nodes; ++node) {
double branchStartX = localToBlock(cubeXOrigin, this.rand.nextInt(Cube.SIZE));
double branchStartY = localToBlock(cubeYOrigin, this.rand.nextInt(Cube.SIZE));
double branchStartZ = localToBlock(cubeZOrigin, this.rand.nextInt(Cube.SIZE));
int subBranches = 1;
if (this.rand.nextInt(LARGE_NODE_RARITY) == 0) {
this.generateLargeNode(cube, this.rand.nextLong(), generatedCubePos,
branchStartX, branchStartY, branchStartZ);
subBranches += this.rand.nextInt(LARGE_NODE_MAX_BRANCHES);
}
for (int branch = 0; branch < subBranches; ++branch) {
float horizDirAngle = this.rand.nextFloat()*(float) Math.PI*2.0F;
float vertDirAngle = (this.rand.nextFloat() - 0.5F)*2.0F/8.0F;
float baseHorizSize = this.rand.nextFloat()*2.0F + this.rand.nextFloat();
if (this.rand.nextInt(BIG_CAVE_RARITY) == 0) {
baseHorizSize *= this.rand.nextFloat()*this.rand.nextFloat()*3.0F + 1.0F;
}
int startWalkedDistance = 0;
int maxWalkedDistance = 0;
double vertCaveSizeMod = 1.0;
this.generateNode(cube, this.rand.nextLong(), generatedCubePos,
branchStartX, branchStartY, branchStartZ,
baseHorizSize, horizDirAngle, vertDirAngle,
startWalkedDistance, maxWalkedDistance, vertCaveSizeMod);
}
}
}
/**
* Generates a flattened cave "room", usually more caves split off it
*/
protected void generateLargeNode(ICubePrimer cube, long seed, CubePos generatedCubePos,
double x, double y, double z) {
float baseHorizSize = 1.0F + this.rand.nextFloat()*6.0F;
float horizDirAngle = 0;
float vertDirAngle = 0;
int startWalkedDistance = -1;
int maxWalkedDistance = -1;
double vertCaveSizeMod = 0.5;
this.generateNode(cube, seed, generatedCubePos, x, y, z,
baseHorizSize, horizDirAngle, vertDirAngle,
startWalkedDistance, maxWalkedDistance, vertCaveSizeMod);
}
/**
* Recursively generates a node in the current cave system tree.
*
* @param cube block buffer to modify
* @param seed random seed to use
* @param generatedCubePos position of the cube to modify
* @param caveX starting x coordinate of the cave
* @param caveY starting Y coordinate of the cave
* @param caveZ starting Z coordinate of the cave
* @param baseCaveSize initial value for cave size, size decreases as cave goes further
* @param horizDirAngle horizontal direction angle
* @param vertCaveSizeMod vertical direction angle
* @param startWalkedDistance the amount of steps the cave already went forwards, used in recursive step. -1 means
* that there will be only one step
* @param maxWalkedDistance maximum distance the cave can go forwards, <= 0 to use default
* @param vertDirAngle changes vertical size of the cave, values < 1 result in flattened caves, > 1 result in
* vertically stretched caves
*/
protected void generateNode(ICubePrimer cube, long seed,
CubePos generatedCubePos,
double caveX, double caveY, double caveZ,
float baseCaveSize, float horizDirAngle, float vertDirAngle,
int startWalkedDistance, int maxWalkedDistance, double vertCaveSizeMod) {
Random rand = new Random(seed);
//store by how much the horizontal and vertical direction angles will change each step
float horizDirChange = 0.0F;
float vertDirChange = 0.0F;
if (maxWalkedDistance <= 0) {
int maxBlockRadius = cubeToMinBlock(this.range - 1);
maxWalkedDistance = maxBlockRadius - rand.nextInt(maxBlockRadius/4);
}
//if true - this branch won't generate new sub-branches
boolean finalStep = false;
int walkedDistance;
if (startWalkedDistance == -1) {
//generate a cave "room"
//start at half distance towards the end = max cave size
walkedDistance = maxWalkedDistance/2;
finalStep = true;
} else {
walkedDistance = startWalkedDistance;
}
int splitPoint = rand.nextInt(maxWalkedDistance/2) + maxWalkedDistance/4;
for (; walkedDistance < maxWalkedDistance; ++walkedDistance) {
float fractionWalked = walkedDistance/(float) maxWalkedDistance;
//horizontal and vertical size of the cave
//size starts small and increases, then decreases as cave goes further
double caveSizeHoriz = CAVE_SIZE_ADD + sin(fractionWalked*(float) Math.PI)*baseCaveSize;
double caveSizeVert = caveSizeHoriz*vertCaveSizeMod;
//Walk forward a single step:
//from sin(alpha)=y/r and cos(alpha)=x/r ==> x = r*cos(alpha) and y = r*sin(alpha)
//always moves by one block in some direction
//here x is xzDirectionFactor, y is yDirectionFactor
float xzDirectionFactor = cos(vertDirAngle);
float yDirectionFactor = sin(vertDirAngle);
//here y is directionZ and x is directionX
caveX += cos(horizDirAngle)*xzDirectionFactor;
caveY += yDirectionFactor;
caveZ += sin(horizDirAngle)*xzDirectionFactor;
if (rand.nextInt(STEEP_STEP_RARITY) == 0) {
vertDirAngle *= STEEPER_FLATTEN_FACTOR;
} else {
vertDirAngle *= FLATTEN_FACTOR;
}
//change the direction
vertDirAngle += vertDirChange*DIRECTION_CHANGE_FACTOR;
horizDirAngle += horizDirChange*DIRECTION_CHANGE_FACTOR;
//update direction change angles
vertDirChange *= PREV_VERT_DIRECTION_CHANGE_WEIGHT;
horizDirChange *= PREV_HORIZ_DIRECTION_CHANGE_WEIGHT;
vertDirChange += (rand.nextFloat() - rand.nextFloat())*rand.nextFloat()*MAX_ADD_DIRECTION_CHANGE_VERT;
horizDirChange += (rand.nextFloat() - rand.nextFloat())*rand.nextFloat()*MAX_ADD_DIRECTION_CHANGE_HORIZ;
//if we reached split point - try to split
//can split only if it's not final branch and the cave is still big enough (>1 block radius)
if (!finalStep && walkedDistance == splitPoint && baseCaveSize > 1.0F) {
this.generateNode(cube, rand.nextLong(),
generatedCubePos, caveX, caveY, caveZ,
rand.nextFloat()*0.5F + 0.5F,//base cave size
horizDirAngle - ((float) Math.PI/2F),//horiz. angle - subtract 90 degrees
vertDirAngle/3.0F, walkedDistance, maxWalkedDistance,
1.0D);
this.generateNode(cube, rand.nextLong(), generatedCubePos, caveX, caveY, caveZ,
rand.nextFloat()*0.5F + 0.5F,//base cave size
horizDirAngle + ((float) Math.PI/2F),//horiz. angle - add 90 degrees
vertDirAngle/3.0F, walkedDistance, maxWalkedDistance,
1.0D);
return;
}
//carve blocks only on some percentage of steps, unless this is the final branch
if (rand.nextInt(CARVE_STEP_RARITY) == 0 && !finalStep) {
continue;
}
double xDist = caveX - generatedCubePos.getXCenter();
double yDist = caveY - generatedCubePos.getYCenter();
double zDist = caveZ - generatedCubePos.getZCenter();
double maxStepsDist = maxWalkedDistance - walkedDistance;
//CHANGE: multiply max(1, vertCaveSizeMod)
double maxDistToCube = baseCaveSize*max(1, vertCaveSizeMod) + CAVE_SIZE_ADD + Cube.SIZE;
//can this cube be reached at all?
//if even after going max distance allowed by remaining steps, it's still too far - stop
//TODO: does it make any performance difference?
if (xDist*xDist + yDist*yDist + zDist*zDist - maxStepsDist*maxStepsDist > maxDistToCube*maxDistToCube) {
return;
}
tryCarveBlocks(cube, generatedCubePos,
caveX, caveY, caveZ,
caveSizeHoriz, caveSizeVert);
if (finalStep) {
return;
}
}
}
//returns true if cave generation should be continued
private void tryCarveBlocks(ICubePrimer cube, CubePos generatedCubePos,
double caveX, double caveY, double caveZ,
double caveSizeHoriz, double caveSizeVert) {
double genCubeCenterX = generatedCubePos.getXCenter();
double genCubeCenterY = generatedCubePos.getYCenter();
double genCubeCenterZ = generatedCubePos.getZCenter();
//Can current step position affect currently modified cube?
//TODO: is multiply by 2 needed?
if (caveX < genCubeCenterX - Cube.SIZE - caveSizeHoriz*2.0D ||
caveY < genCubeCenterY - Cube.SIZE - caveSizeVert*2.0D ||
caveZ < genCubeCenterZ - Cube.SIZE - caveSizeHoriz*2.0D ||
caveX > genCubeCenterX + Cube.SIZE + caveSizeHoriz*2.0D ||
caveY > genCubeCenterY + Cube.SIZE + caveSizeVert*2.0D ||
caveZ > genCubeCenterZ + Cube.SIZE + caveSizeHoriz*2.0D) {
return;
}
int minLocalX = floor(caveX - caveSizeHoriz) - generatedCubePos.getMinBlockX() - 1;
int maxLocalX = floor(caveX + caveSizeHoriz) - generatedCubePos.getMinBlockX() + 1;
int minLocalY = floor(caveY - caveSizeVert) - generatedCubePos.getMinBlockY() - 1;
int maxLocalY = floor(caveY + caveSizeVert) - generatedCubePos.getMinBlockY() + 1;
int minLocalZ = floor(caveZ - caveSizeHoriz) - generatedCubePos.getMinBlockZ() - 1;
int maxLocalZ = floor(caveZ + caveSizeHoriz) - generatedCubePos.getMinBlockZ() + 1;
//skip is if everything is outside of that cube
if (maxLocalX <= 0 || minLocalX >= Cube.SIZE ||
maxLocalY <= 0 || minLocalY >= Cube.SIZE ||
maxLocalZ <= 0 || minLocalZ >= Cube.SIZE) {
return;
}
StructureBoundingBox boundingBox = new StructureBoundingBox(minLocalX, minLocalY, minLocalZ, maxLocalX, maxLocalY, maxLocalZ);
StructureGenUtil.clampBoundingBoxToLocalCube(boundingBox);
boolean hitLiquid = scanWallsForBlock(cube, boundingBox,
(b) -> b.getBlock() == Blocks.LAVA || b.getBlock() == Blocks.FLOWING_LAVA);
if (!hitLiquid) {
carveBlocks(cube, generatedCubePos, caveX, caveY, caveZ, caveSizeHoriz, caveSizeVert, boundingBox);
}
}
private void carveBlocks(ICubePrimer cube,
CubePos generatedCubePos,
double caveX, double caveY, double caveZ,
double caveSizeHoriz, double caveSizeVert,
StructureBoundingBox boundingBox) {
int generatedCubeX = generatedCubePos.getX();
int generatedCubeY = generatedCubePos.getY();
int generatedCubeZ = generatedCubePos.getZ();
int minX = boundingBox.minX;
int maxX = boundingBox.maxX;
int minY = boundingBox.minY;
int maxY = boundingBox.maxY;
int minZ = boundingBox.minZ;
int maxZ = boundingBox.maxZ;
for (int localX = minX; localX < maxX; ++localX) {
double distX = normalizedDistance(generatedCubeX, localX, caveX, caveSizeHoriz);
for (int localZ = minZ; localZ < maxZ; ++localZ) {
double distZ = normalizedDistance(generatedCubeZ, localZ, caveZ, caveSizeHoriz);
if (distX*distX + distZ*distZ >= 1.0D) {
continue;
}
for (int localY = minY; localY < maxY; ++localY) {
double distY = normalizedDistance(generatedCubeY, localY, caveY, caveSizeVert);
IBlockState state = cube.getBlockState(localX, localY, localZ);
if (!isBlockReplaceable.test(state)) {
continue;
}
if (shouldCarveBlock(distX, distY, distZ)) {
// No lava generation, infinite depth. Lava will be generated differently (or not generated)
cube.setBlockState(localX, localY, localZ, Blocks.AIR.getDefaultState());
} else if (state.getBlock() == Blocks.DIRT) {
//vanilla dirt-grass replacement works by scanning top-down and moving the block
//cubic chunks needs to be a bit more hacky about it
//instead of keeping track of the encountered grass block
//cubic chunks replaces any dirt block (it's before population, no ore-like dirt formations yet)
//with grass, if the block above would be deleted by this cave generator step
double distYAbove = normalizedDistance(generatedCubeY, localY + 1, caveY, caveSizeVert);
if (shouldCarveBlock(distX, distYAbove, distZ)) {
cube.setBlockState(localX, localY, localZ, Blocks.GRASS.getDefaultState());
}
}
}
}
}
}
private static boolean shouldCarveBlock(double distX, double distY, double distZ) {
//distY > CAVE_FLOOR_DEPTH --> flattened floor
return distY > CAVE_FLOOR_DEPTH && distX*distX + distY*distY + distZ*distZ < 1.0D;
}
}