Skip to content

Commit

Permalink
Document tree generation code
Browse files Browse the repository at this point in the history
  • Loading branch information
thedarkcolour committed Dec 18, 2024
1 parent 4122a11 commit 1a38c61
Show file tree
Hide file tree
Showing 40 changed files with 376 additions and 329 deletions.
9 changes: 2 additions & 7 deletions src/main/java/forestry/arboriculture/VanillaWoodType.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,12 @@
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;

import com.mojang.authlib.GameProfile;

import forestry.api.arboriculture.IWoodType;
import forestry.api.arboriculture.genetics.IFruit;
import forestry.api.genetics.IGenome;
import forestry.api.genetics.alleles.TreeChromosomes;
import forestry.arboriculture.blocks.ForestryLeafType;
import forestry.arboriculture.features.ArboricultureBlocks;
import forestry.modules.features.FeatureBlockGroup;

import org.jetbrains.annotations.Nullable;

Expand All @@ -32,7 +26,8 @@ public enum VanillaWoodType implements IWoodType {
BIRCH(ForestryLeafType.BIRCH),
JUNGLE(ForestryLeafType.JUNGLE),
ACACIA(ForestryLeafType.ACACIA_VANILLA),
DARK_OAK(ForestryLeafType.DARK_OAK);
DARK_OAK(ForestryLeafType.DARK_OAK),
CHERRY(ForestryLeafType.CHERRY_VANILLA);

public static final VanillaWoodType[] VALUES = values();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public final class ForestryLeafType implements IBlockSubtype {
public static final ForestryLeafType LIME = new ForestryLeafType(ForestryTreeSpecies.LIME);
public static final ForestryLeafType WALNUT = new ForestryLeafType(ForestryTreeSpecies.WALNUT);
public static final ForestryLeafType CHESTNUT = new ForestryLeafType(ForestryTreeSpecies.CHESTNUT);
public static final ForestryLeafType CHERRY_VANILLA = new ForestryLeafType(ForestryTreeSpecies.CHERRY_VANILLA);
public static final ForestryLeafType BUSH_CHERRY = new ForestryLeafType(ForestryTreeSpecies.BUSH_CHERRY);
public static final ForestryLeafType LEMON = new ForestryLeafType(ForestryTreeSpecies.LEMON);
public static final ForestryLeafType PLUM = new ForestryLeafType(ForestryTreeSpecies.PLUM);
Expand Down
48 changes: 31 additions & 17 deletions src/main/java/forestry/arboriculture/genetics/TreeGrowthHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,49 @@

import forestry.api.genetics.IGenome;

import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap;

public class TreeGrowthHelper {
/**
* Finds a valid growth position for a tree if it has all necessary saplings and at least one position has enough
* room for the tree to grow. If no valid position is found, then {@code null} is returned.
*
*
* @param level The world to check for valid positions.
* @param genome The genome of the sapling trying to grow.
* @param pos The position of the sapling.
* @param expectedGirth The expected girth of the tree according to genetics and variation.
* @param expectedHeight The expected height of the tree according to genetics and variation.
* @return A valid growth position, or {@code null} if saplings were missing or if there's no room for the tree.
*/
@Nullable
public static BlockPos getGrowthPos(LevelAccessor world, IGenome genome, BlockPos pos, int expectedGirth, int expectedHeight) {
BlockPos growthPos = hasSufficientSaplingsAroundSapling(genome, world, pos, expectedGirth);
public static BlockPos getGrowthPos(LevelAccessor level, IGenome genome, BlockPos pos, int expectedGirth, int expectedHeight) {
// TODO use MutableBlockPos to reduce BlockPos allocations.
// Check if the tree has enough saplings to grow
BlockPos growthPos = hasSufficientSaplingsAroundSapling(genome, level, pos, expectedGirth);
if (growthPos == null) {
return null;
}

if (!hasRoom(world, growthPos, expectedGirth, expectedHeight)) {
// Check if the trunk would be obstructed by solid blocks
if (!hasRoom(level, growthPos, expectedGirth, expectedHeight)) {
return null;
}

return growthPos;
}

private static boolean hasRoom(LevelAccessor world, BlockPos pos, int expectedGirth, int expectedHeight) {
private static boolean hasRoom(LevelAccessor level, BlockPos pos, int expectedGirth, int expectedHeight) {
Vec3i area = new Vec3i(expectedGirth, expectedHeight + 1, expectedGirth);
return checkArea(world, pos.above(), area);
return checkArea(level, pos.above(), area);
}

private static boolean checkArea(LevelAccessor world, BlockPos start, Vec3i area) {
private static boolean checkArea(LevelAccessor level, BlockPos start, Vec3i area) {
for (int x = 0; x < area.getX(); x++) {
for (int y = 0; y < area.getY(); y++) {
for (int z = 0; z < area.getZ(); z++) {
BlockPos pos = start.offset(x, y, z);
BlockState blockState = world.getBlockState(pos);
//TODO: Can't be used because the world generation only provides a IWorld and not a World
/*BlockItemUseContext context = new DirectionalPlaceContext((World) world, pos, Direction.DOWN, ItemStack.EMPTY, Direction.UP);
return blockState.isReplaceable(context);*/
BlockState blockState = level.getBlockState(pos);
if (!blockState.canBeReplaced()) {
return false;
}
Expand All @@ -63,7 +77,7 @@ private static boolean checkArea(LevelAccessor world, BlockPos start, Vec3i area
private static BlockPos hasSufficientSaplingsAroundSapling(IGenome genome, LevelAccessor world, BlockPos saplingPos, int expectedGirth) {
final int checkSize = (expectedGirth * 2) - 1;
final int offset = expectedGirth - 1;
final Map<BlockPos, Boolean> knownSaplings = new HashMap<>(checkSize * checkSize);
final Object2BooleanOpenHashMap<BlockPos> knownSaplings = new Object2BooleanOpenHashMap<>(checkSize * checkSize);

for (int x = -offset; x <= 0; x++) {
for (int z = -offset; z <= 0; z++) {
Expand All @@ -77,11 +91,11 @@ private static BlockPos hasSufficientSaplingsAroundSapling(IGenome genome, Level
return null;
}

private static boolean checkForSaplings(IGenome genome, LevelAccessor world, BlockPos startPos, int girth, Map<BlockPos, Boolean> knownSaplings) {
private static boolean checkForSaplings(IGenome genome, LevelAccessor level, BlockPos startPos, int girth, Object2BooleanOpenHashMap<BlockPos> knownSaplings) {
for (int x = 0; x < girth; x++) {
for (int z = 0; z < girth; z++) {
BlockPos checkPos = startPos.offset(x, 0, z);
Boolean knownSapling = knownSaplings.computeIfAbsent(checkPos, k -> isSapling(genome, world, checkPos));
boolean knownSapling = knownSaplings.computeIfAbsent(checkPos, k -> isSapling(genome, level, checkPos));
if (!knownSapling) {
return false;
}
Expand All @@ -90,16 +104,16 @@ private static boolean checkForSaplings(IGenome genome, LevelAccessor world, Blo
return true;
}

private static boolean isSapling(IGenome genome, LevelAccessor world, BlockPos pos) {
if (!world.hasChunkAt(pos)) {
private static boolean isSapling(IGenome genome, LevelAccessor level, BlockPos pos) {
if (!level.hasChunkAt(pos)) {
return false;
}

if (world.isEmptyBlock(pos)) {
if (level.isEmptyBlock(pos)) {
return false;
}

TileSapling sapling = TileUtil.getTile(world, pos, TileSapling.class);
TileSapling sapling = TileUtil.getTile(level, pos, TileSapling.class);
if (sapling == null) {
return false;
}
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/forestry/arboriculture/worldgen/FeatureAcacia.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ public FeatureAcacia(ITreeGenData tree) {
}

@Override
public Set<BlockPos> generateTrunk(LevelAccessor world, RandomSource rand, TreeBlockTypeLog wood, BlockPos startPos) {
public Set<BlockPos> generateTrunk(LevelAccessor level, RandomSource rand, TreeBlockTypeLog wood, BlockPos startPos) {
Direction leanDirection = FeatureHelper.DirectionHelper.getRandom(rand);
float leanAmount = height / 4.0f;

Set<BlockPos> treeTops = FeatureHelper.generateTreeTrunk(world, rand, wood, startPos, height, girth, 0, 0, leanDirection, leanAmount);
Set<BlockPos> treeTops = FeatureHelper.generateTreeTrunk(level, rand, wood, startPos, height, girth, 0, 0, leanDirection, leanAmount);
if (height > 5 && rand.nextBoolean()) {
Direction branchDirection = FeatureHelper.DirectionHelper.getRandomOther(rand, leanDirection);
Set<BlockPos> treeTops2 = FeatureHelper.generateTreeTrunk(world, rand, wood, startPos, Math.round(height * 0.66f), girth, 0, 0, branchDirection, leanAmount);
Set<BlockPos> treeTops2 = FeatureHelper.generateTreeTrunk(level, rand, wood, startPos, Math.round(height * 0.66f), girth, 0, 0, branchDirection, leanAmount);
treeTops.addAll(treeTops2);
}

Expand All @@ -53,22 +53,22 @@ public Set<BlockPos> generateTrunk(LevelAccessor world, RandomSource rand, TreeB
float canopyWidth = rand.nextBoolean() ? 3.0f : 2.5f;
int radius = Math.round(canopyMultiplier * canopyWidth - 4);
BlockPos pos = new BlockPos(xOffset, startPos.getY() + yOffset - canopyThickness, zOffset);
branchEnds.addAll(FeatureHelper.generateBranches(world, rand, wood, pos, girth, 0.0f, 0.1f, radius, 2, 1.0f));
branchEnds.addAll(FeatureHelper.generateBranches(level, rand, wood, pos, girth, 0.0f, 0.1f, radius, 2, 1.0f));
}

return branchEnds;
}

@Override
protected void generateLeaves(LevelAccessor world, RandomSource rand, TreeBlockTypeLeaf leaf, TreeContour contour, BlockPos startPos) {
protected void generateLeaves(LevelAccessor level, RandomSource rand, TreeBlockTypeLeaf leaf, TreeContour contour, BlockPos startPos) {
for (BlockPos branchEnd : contour.getBranchEnds()) {
int leafSpawn = branchEnd.getY() - startPos.getY();
int canopyThickness = Math.max(1, Math.round(leafSpawn / 10.0f));
float canopyMultiplier = (1.5f * height - leafSpawn + 2) / 4.0f;
float canopyWidth = rand.nextBoolean() ? 1.0f : 1.5f;
BlockPos center = new BlockPos(branchEnd.getX(), leafSpawn - canopyThickness + 1 + startPos.getY(), branchEnd.getZ());
float radius = Math.max(1, canopyMultiplier * canopyWidth + girth);
FeatureHelper.generateCylinderFromPos(world, leaf, center, radius, canopyThickness, FeatureHelper.EnumReplaceMode.AIR, contour);
FeatureHelper.generateCylinderFromPos(level, leaf, center, radius, canopyThickness, FeatureHelper.EnumReplaceMode.AIR, contour);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelWriter;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
Expand All @@ -36,6 +37,19 @@
import forestry.core.utils.VecUtil;
import forestry.core.worldgen.FeatureBase;

/**
* Base logic for tree generation. Tree generation generally follows these steps:
* <ol>
* <li>Calculate the girth and height based on species, genome, and random variation.</li>
* <li>Check if there are enough saplings to grow the tree and if the trunk has enough room to grow.</li>
* <li>Remove the saplings.</li>
* <li>Generate the trunk and branches. Keep track of the "branch end" positions for leaf placement.</li>
* <li>Generate leaves at the branch positions.</li>
* <li>If the tree has pod fruit, generate fruit pods as well.</li>
* <li>Update distance states for all leaves after generation.</li>
* <li>Calls updateShape on all leaf blocks, which handles decay and waterlogged behavior.</li>
* </ol>
*/
public abstract class FeatureArboriculture extends FeatureBase {
protected static final int minPodHeight = 3;

Expand All @@ -47,46 +61,60 @@ protected FeatureArboriculture(ITreeGenData tree) {

@Override
public IGenome getDefaultGenome() {
return tree.getDefaultGenome();
return this.tree.getDefaultGenome();
}

@Override
public boolean place(IGenome genome, LevelAccessor world, RandomSource rand, BlockPos pos, boolean forced) {
TreeBlockTypeLeaf leaf = new TreeBlockTypeLeaf(tree, genome);
TreeBlockTypeLog wood = new TreeBlockTypeLog(tree, genome);
public boolean place(IGenome genome, LevelAccessor level, RandomSource rand, BlockPos pos, boolean forced) {
TreeBlockTypeLeaf leaf = new TreeBlockTypeLeaf(this.tree, genome);
TreeBlockTypeLog wood = new TreeBlockTypeLog(this.tree, genome);

preGenerate(genome, world, rand, pos);
// Calculate height and girth
preGenerate(genome, level, rand, pos);

// Determine valid growth position if any, or skip all checks if forced is true
BlockPos genPos;
if (forced) {
genPos = pos;
} else {
genPos = getValidGrowthPos(world, pos);
// Default implementation is in TreeGrowthHelper.getGrowthPos, but can be overridden in TreeSpecies
genPos = getValidGrowthPos(level, pos);
}

// If a valid growth position was found
if (genPos != null) {
clearSaplings(world, genPos);
List<BlockPos> branchEnds = new ArrayList<>(generateTrunk(world, rand, wood, genPos));
// Remove all saplings
clearSaplings(level, genPos);

// Generate a trunk and a list of branch end positions. Store those branch ends in a contour
ArrayList<BlockPos> branchEnds = new ArrayList<>(generateTrunk(level, rand, wood, genPos));
branchEnds.sort(VecUtil.TOP_DOWN_COMPARATOR);
TreeContour.Impl contour = new TreeContour.Impl(branchEnds);
generateLeaves(world, rand, leaf, contour, genPos);
generateExtras(world, rand, genPos);
updateLeaves(world, contour);
DiscreteVoxelShape voxelshapepart = this.updateLeaves(world, contour);
StructureTemplate.updateShapeAtEdge(world, 3, voxelshapepart, contour.boundingBox.minX(), contour.boundingBox.minY(), contour.boundingBox.minZ());

// Generate leaves and pods
generateLeaves(level, rand, leaf, contour, genPos);
generateExtras(level, rand, genPos);

// Correctly update the leaf distance states on the leaf blocks
DiscreteVoxelShape voxelshapepart = updateLeaves(level, contour);
// Call updateShape method on all blocks on the edge of the tree's bounding box
StructureTemplate.updateShapeAtEdge(level, 3, voxelshapepart, contour.boundingBox.minX(), contour.boundingBox.minY(), contour.boundingBox.minZ());
return true;
}

return false;
}

public void preGenerate(IGenome genome, LevelAccessor world, RandomSource rand, BlockPos startPos) {
/**
* Used by {@link FeatureTree} to set fields such as height and girth before generating anything in the world.
*/
public void preGenerate(IGenome genome, LevelAccessor level, RandomSource rand, BlockPos startPos) {
}

/**
* Copied vanilla logic from TreeFeature#updateLeaves
*/
private DiscreteVoxelShape updateLeaves(LevelAccessor world, TreeContour.Impl contour) {
private DiscreteVoxelShape updateLeaves(LevelAccessor level, TreeContour.Impl contour) {
BoundingBox boundingBox = contour.boundingBox;
List<Set<BlockPos>> list = Lists.newArrayList();
DiscreteVoxelShape voxelshapepart = new BitSetDiscreteVoxelShape(boundingBox.getXSpan(), boundingBox.getYSpan(), boundingBox.getZSpan());
Expand All @@ -106,10 +134,9 @@ private DiscreteVoxelShape updateLeaves(LevelAccessor world, TreeContour.Impl co
for (Direction direction : Direction.values()) {
blockpos$mutable.setWithOffset(blockpos1, direction);
if (!contour.leavePositions.contains(blockpos$mutable)) {
BlockState blockstate = world.getBlockState(blockpos$mutable);
BlockState blockstate = level.getBlockState(blockpos$mutable);
if (blockstate.hasProperty(BlockStateProperties.DISTANCE)) {
list.get(0).add(blockpos$mutable.immutable());
// TreeFeature.setBlockKnownShape(world, blockpos$mutable, blockstate.setValue(BlockStateProperties.DISTANCE, 1));
if (boundingBox.isInside(blockpos$mutable)) {
voxelshapepart.fill(blockpos$mutable.getX() - boundingBox.minX(), blockpos$mutable.getY() - boundingBox.minY(), blockpos$mutable.getZ() - boundingBox.minZ());
}
Expand All @@ -130,12 +157,12 @@ private DiscreteVoxelShape updateLeaves(LevelAccessor world, TreeContour.Impl co
for (Direction direction1 : Direction.values()) {
blockpos$mutable.setWithOffset(blockpos2, direction1);
if (!set.contains(blockpos$mutable) && !set1.contains(blockpos$mutable)) {
BlockState blockstate1 = world.getBlockState(blockpos$mutable);
BlockState blockstate1 = level.getBlockState(blockpos$mutable);
if (blockstate1.hasProperty(BlockStateProperties.DISTANCE)) {
int k = blockstate1.getValue(BlockStateProperties.DISTANCE);
if (k > l + 1) {
BlockState blockstate2 = blockstate1.setValue(BlockStateProperties.DISTANCE, Integer.valueOf(l + 1));
// TreeFeature.setBlockKnownShape(world, blockpos$mutable, blockstate2);
setBlockKnownShape(level, blockpos$mutable, blockstate2);
if (boundingBox.isInside(blockpos$mutable)) {
voxelshapepart.fill(blockpos$mutable.getX() - boundingBox.minX(), blockpos$mutable.getY() - boundingBox.minY(), blockpos$mutable.getZ() - boundingBox.minZ());
}
Expand All @@ -151,33 +178,38 @@ private DiscreteVoxelShape updateLeaves(LevelAccessor world, TreeContour.Impl co
return voxelshapepart;
}

private static void setBlockKnownShape(LevelWriter level, BlockPos pos, BlockState state) {
level.setBlock(pos, state, BlockSapling.UPDATE_NEIGHBORS | BlockSapling.UPDATE_CLIENTS | BlockSapling.UPDATE_KNOWN_SHAPE);
}

/**
* Generate the tree's trunk. Returns a list of positions of branch ends for leaves to generate at.
*/
protected abstract Set<BlockPos> generateTrunk(LevelAccessor level, RandomSource rand, TreeBlockTypeLog wood, BlockPos startPos);

protected abstract Set<BlockPos> generateTrunk(LevelAccessor world, RandomSource rand, TreeBlockTypeLog wood, BlockPos startPos);

protected abstract void generateLeaves(LevelAccessor world, RandomSource rand, TreeBlockTypeLeaf leaf, TreeContour contour, BlockPos startPos);
protected abstract void generateLeaves(LevelAccessor level, RandomSource rand, TreeBlockTypeLeaf leaf, TreeContour contour, BlockPos startPos);

protected abstract void generateExtras(LevelAccessor world, RandomSource rand, BlockPos startPos);
protected abstract void generateExtras(LevelAccessor level, RandomSource rand, BlockPos startPos);

@Nullable
public abstract BlockPos getValidGrowthPos(LevelAccessor world, BlockPos pos);
public abstract BlockPos getValidGrowthPos(LevelAccessor level, BlockPos pos);

public void clearSaplings(LevelAccessor world, BlockPos genPos) {
int treeGirth = tree.getGirth(tree.getDefaultGenome());
/**
* Removes all saplings before generating the trunk.
*/
public void clearSaplings(LevelAccessor level, BlockPos genPos) {
int treeGirth = this.tree.getGirth(this.tree.getDefaultGenome());
for (int x = 0; x < treeGirth; x++) {
for (int z = 0; z < treeGirth; z++) {
BlockPos saplingPos = genPos.offset(x, 0, z);
if (world.getBlockState(saplingPos).getBlock() instanceof BlockSapling) {
world.setBlock(saplingPos, Blocks.AIR.defaultBlockState(), 18);
if (level.getBlockState(saplingPos).getBlock() instanceof BlockSapling) {
level.setBlock(saplingPos, Blocks.AIR.defaultBlockState(), 18);
}
}
}
}

public boolean hasPods() {
return tree.allowsFruitBlocks(tree.getDefaultGenome());
return this.tree.allowsFruitBlocks(this.tree.getDefaultGenome());
}

}
Loading

0 comments on commit 1a38c61

Please sign in to comment.