Drive basic trees from biome feature stages

This commit is contained in:
Daniar Mannanov 2026-08-10 23:36:12 +03:00
parent 2bfa3155f3
commit 064f0f1cca
5 changed files with 339 additions and 41 deletions

View file

@ -33,7 +33,7 @@ const dataVersion26 = 4790
// first time it ran: chunkAt prefers the store over the generator, so the
// already-explored area around spawn keeps its old terrain and every later fix
// looks like it did nothing in exactly the place you are standing.
const generatorVersion = 14
const generatorVersion = 15
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
// because it is ours, not part of the vanilla chunk format.

127
internal/world/trees.go Normal file
View file

@ -0,0 +1,127 @@
package world
import "regionio/internal/worldgen"
const vegetationStage = 9
func placeVanillaTrees(c *Chunk, seed int64, cx, cz int32, biomes *[16][16]string, surfTop *[16][16]int) {
set, err := worldgen.LoadFeatureSet()
if err != nil {
panic(err)
}
random, decorationSeed := worldgen.DecorationRandom(seed, int(cx), int(cz))
seen := make(map[string]bool)
for bx := 0; bx < 16; bx += 4 {
for bz := 0; bz < 16; bz += 4 {
stages := set.Biomes[biomes[bx][bz]].Features
if len(stages) <= vegetationStage {
continue
}
for featureIndex, name := range stages[vegetationStage] {
if seen[name] {
continue
}
seen[name] = true
placed, ok := set.Placed[name]
if !ok || !treeFeatureType(set.Configured[placed.Feature].Type) {
continue
}
plan, err := set.Placement(name)
if err != nil {
continue
}
random.SetFeatureSeed(decorationSeed, featureIndex, vegetationStage)
count := plan.Count.Sample(random)
for attempt := 0; attempt < count; attempt++ {
x, z := int(random.NextIntN(16)), int(random.NextIntN(16))
y := MinY + surfTop[x][z] + 1
placeTreeReference(c, set, random, placed.Feature, x, y, z)
}
}
}
}
}
func treeFeatureType(kind string) bool {
return kind == "minecraft:tree" || kind == "minecraft:random_selector"
}
func placeTreeReference(c *Chunk, set *worldgen.FeatureSet, random worldgen.RandomSource, name string, x, y, z int) bool {
configured, ok := set.Configured[name]
if !ok {
if placed, exists := set.Placed[name]; exists {
return placeTreeReference(c, set, random, placed.Feature, x, y, z)
}
return false
}
switch configured.Type {
case "minecraft:random_selector":
selector, err := set.RandomSelector(name)
if err != nil {
return false
}
for _, entry := range selector.Features {
if random.NextFloat() < entry.Chance {
return placeTreeReference(c, set, random, entry.Feature.Name, x, y, z)
}
}
return placeTreeReference(c, set, random, selector.Default.Name, x, y, z)
case "minecraft:tree":
config, err := set.Tree(name)
if err != nil || config.TrunkPlacer.Type != "minecraft:straight_trunk_placer" || config.FoliagePlacer.Type != "minecraft:blob_foliage_placer" {
return false
}
return placeStraightBlobTree(c, random, x, y, z, config)
}
return false
}
func placeStraightBlobTree(c *Chunk, random worldgen.RandomSource, x, y, z int, config worldgen.TreeFeatureConfig) bool {
if x < 2 || x >= 14 || z < 2 || z >= 14 || y <= MinY || y+12 >= MinY+WorldHeight {
return false
}
floor := c.GetBlock(x, y-1, z)
if floor != StateGrass && floor != StateDirt && floor != StateCoarseDirt && floor != StatePodzol {
return false
}
height := config.TrunkPlacer.BaseHeight
if config.TrunkPlacer.HeightRandA > 0 {
height += int(random.NextIntN(int32(config.TrunkPlacer.HeightRandA + 1)))
}
if config.TrunkPlacer.HeightRandB > 0 {
height += int(random.NextIntN(int32(config.TrunkPlacer.HeightRandB + 1)))
}
for dy := 0; dy <= height+config.FoliagePlacer.Height; dy++ {
if c.GetBlock(x, y+dy, z) != StateAir {
return false
}
}
logState, okLog := nameToStateID(config.TrunkProvider.State.Name, config.TrunkProvider.State.Properties)
leafState, okLeaf := nameToStateID(config.FoliageProvider.State.Name, config.FoliageProvider.State.Properties)
if !okLog || !okLeaf {
return false
}
c.SetBlock(x, y-1, z, StateDirt)
for dy := 0; dy < height; dy++ {
c.SetBlock(x, y+dy, z, logState)
}
centerY := y + height - 1 + config.FoliagePlacer.Offset
for layer := 0; layer < config.FoliagePlacer.Height; layer++ {
radius := config.FoliagePlacer.Radius
if layer == config.FoliagePlacer.Height-1 {
radius--
}
ly := centerY - layer
for dx := -radius; dx <= radius; dx++ {
for dz := -radius; dz <= radius; dz++ {
if abs(dx) == radius && abs(dz) == radius && random.NextIntN(2) == 0 {
continue
}
if c.GetBlock(x+dx, ly, z+dz) == StateAir {
c.SetBlock(x+dx, ly, z+dz, leafState)
}
}
}
}
return true
}

View file

@ -0,0 +1,61 @@
package world
import (
"testing"
"regionio/internal/worldgen"
)
func TestStraightBlobTreeFromDatapack(t *testing.T) {
set, err := worldgen.LoadFeatureSet()
if err != nil {
t.Fatal(err)
}
config, err := set.Tree("minecraft:oak")
if err != nil {
t.Fatal(err)
}
chunk := NewChunk(0, 0, BiomePlains)
chunk.setBlockRaw(8, 63, 8, StateGrass)
if !placeStraightBlobTree(chunk, worldgen.NewLegacy(42), 8, 64, 8, config) {
t.Fatal("datapack oak placement failed")
}
logs, leaves := 0, 0
for y := 64; y < 80; y++ {
for x := 0; x < 16; x++ {
for z := 0; z < 16; z++ {
switch chunk.GetBlock(x, y, z) {
case StateOakLog:
logs++
case StateOakLeaf:
leaves++
}
}
}
}
if logs < 4 || leaves < 10 {
t.Fatalf("tree has %d logs and %d leaves", logs, leaves)
}
}
func TestBiomeTreeStageProducesTrees(t *testing.T) {
gen := NewVanillaGenerator(12345)
trees := 0
for cx := int32(-12); cx <= 12; cx += 3 {
for cz := int32(-12); cz <= 12; cz += 3 {
chunk := gen(cx, cz)
for y := SeaLevel; y < 160; y++ {
for x := 0; x < 16; x++ {
for z := 0; z < 16; z++ {
if chunk.GetBlock(x, y, z) == StateOakLog {
trees++
}
}
}
}
}
}
if trees == 0 {
t.Fatal("biome vegetation stages produced no oak logs")
}
}

View file

@ -480,50 +480,15 @@ func decorate(c *Chunk, od *worldgen.OverworldDensity, cx, cz int32, seed int64,
placeVanillaOres(c, seed, cx, cz, biomeName)
placeVanillaSprings(c, seed, cx, cz, biomeName)
placeVanillaTrees(c, seed, cx, cz, biomeName, surfTop)
placeFlora(c, &r, surfTop, grass, biomeName)
placeDesertFeatures(c, &r, surfTop, biomeName)
placeRocks(c, &r, surfTop, grass, biomeName)
const attempts = 8
for a := 0; a < attempts; a++ {
lx := 2 + int(r.next()%12)
lz := 2 + int(r.next()%12)
if !grass[lx][lz] {
continue
}
baseY := MinY + surfTop[lx][lz] + 1
placeOak(c, lx, baseY, lz, &r)
}
// Place large structures like villages and strongholds
worldgen.PlaceStructures(c, od, cx, cz, seed, surfTop, biomeName)
}
func placeOak(c *Chunk, lx, baseY, lz int, r *chunkRand) {
h := 4 + int(r.next()%3) // trunk height 4..6
for i := 0; i < h; i++ {
c.SetBlock(lx, baseY+i, lz, StateOakLog)
}
topY := baseY + h - 1
// Canopy: two wide layers around the top, then two narrow layers above.
layers := []struct {
dy, radius int
}{{-1, 2}, {0, 2}, {1, 1}, {2, 1}}
for _, ly := range layers {
y := topY + ly.dy
for dx := -ly.radius; dx <= ly.radius; dx++ {
for dz := -ly.radius; dz <= ly.radius; dz++ {
if ly.radius == 2 && abs(dx) == 2 && abs(dz) == 2 {
continue // trim the far corners for a rounder shape
}
if c.GetBlock(lx+dx, y, lz+dz) == StateAir {
c.SetBlock(lx+dx, y, lz+dz, StateOakLeaf)
}
}
}
}
}
func abs(v int) int {
if v < 0 {
return -v