RegionIO/internal/world/vegetation_patches.go

305 lines
10 KiB
Go

package world
import (
"strconv"
"regionio/internal/worldgen"
)
func (r *decorationRegion) placeScheduledVegetationPatches(seed int64) error {
set, err := worldgen.LoadFeatureSet()
if err != nil {
return err
}
if err := r.ensureSourceNeighborhood(); err != nil {
return err
}
schedule, err := set.FeatureSchedule(possibleBiomeOrder(), r.sourceBiomes(), vegetationStage)
if err != nil {
return err
}
random, decorationSeed := worldgen.DecorationRandom(seed, int(r.sourceX), int(r.sourceZ))
origin := worldgen.FeaturePosition{X: int(r.sourceX) << 4, Y: MinY, Z: int(r.sourceZ) << 4}
for _, scheduled := range schedule {
placed, ok := set.Placed[scheduled.Name]
if !ok {
continue
}
configured, ok := set.Configured[placed.Feature]
if !ok {
continue
}
random.SetFeatureSeed(decorationSeed, scheduled.Index, vegetationStage)
context := r.placementContext(func(position worldgen.FeaturePosition) bool {
return r.biomeAllowsFeature(set, scheduled.Name, vegetationStage, position)
})
switch configured.Type {
case "minecraft:vegetation_patch":
config, err := set.VegetationPatch(placed.Feature)
if err != nil {
return err
}
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
r.placeVegetationPatch(random, position, config, set)
return nil
}); err != nil {
return err
}
case "minecraft:kelp":
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
r.placeKelp(random, position, set)
return nil
}); err != nil {
return err
}
case "minecraft:seagrass":
config, err := set.Probability(placed.Feature)
if err != nil {
return err
}
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
r.placeSeagrass(random, position, config.Probability, set)
return nil
}); err != nil {
return err
}
}
}
return nil
}
func (r *decorationRegion) placeKelp(random worldgen.RandomSource, position worldgen.FeaturePosition, set *worldgen.FeatureSet) bool {
position.Y = r.heightAt("OCEAN_FLOOR", position.X, position.Z)
if r.getBlock(position.X, position.Y, position.Z) != StateWater {
return false
}
plant, plantOK := nameToStateID("minecraft:kelp_plant", nil)
if !plantOK {
return false
}
height := 1 + int(random.NextIntN(10))
placed := false
for i := 0; i <= height; i++ {
y := position.Y + i
if r.getBlock(position.X, y, position.Z) == StateWater && r.getBlock(position.X, y+1, position.Z) == StateWater &&
r.canKelpSurvive(position.X, y, position.Z, set) {
if i == height {
age := 20 + int(random.NextIntN(4))
head, ok := nameToStateID("minecraft:kelp", map[string]string{"age": strconv.Itoa(age)})
if !ok || !r.setBlock(position.X, y, position.Z, head) {
break
}
} else if !r.setBlock(position.X, y, position.Z, plant) {
break
}
placed = true
continue
}
if i > 0 {
belowY := y - 1
if r.canKelpSurvive(position.X, belowY, position.Z, set) && r.getBlock(position.X, belowY-1, position.Z) != plant {
age := 20 + int(random.NextIntN(4))
head, ok := nameToStateID("minecraft:kelp", map[string]string{"age": strconv.Itoa(age)})
if ok {
placed = r.setBlock(position.X, belowY, position.Z, head) || placed
}
}
}
break
}
return placed
}
func (r *decorationRegion) canKelpSurvive(x, y, z int, set *worldgen.FeatureSet) bool {
below := r.getBlock(x, y-1, z)
state, ok := stateByID(below)
if !ok || blockTagContains(set, "minecraft:cannot_support_kelp", state.Name) {
return false
}
return state.Name == "minecraft:kelp" || state.Name == "minecraft:kelp_plant" || fullSolidState(below)
}
func (r *decorationRegion) placeSeagrass(random worldgen.RandomSource, position worldgen.FeaturePosition, probability float32, set *worldgen.FeatureSet) bool {
position.X += int(random.NextIntN(8)) - int(random.NextIntN(8))
position.Z += int(random.NextIntN(8)) - int(random.NextIntN(8))
position.Y = r.heightAt("OCEAN_FLOOR", position.X, position.Z)
if r.getBlock(position.X, position.Y, position.Z) != StateWater {
return false
}
tall := random.NextDouble() < float64(probability)
if tall {
lower, lowerOK := nameToStateID("minecraft:tall_seagrass", map[string]string{"half": "lower"})
upper, upperOK := nameToStateID("minecraft:tall_seagrass", map[string]string{"half": "upper"})
if !lowerOK || !upperOK || r.getBlock(position.X, position.Y+1, position.Z) != StateWater || !r.canSeagrassSurvive(position.X, position.Y, position.Z, set) {
return false
}
return r.setBlock(position.X, position.Y, position.Z, lower) && r.setBlock(position.X, position.Y+1, position.Z, upper)
}
short, ok := nameToStateID("minecraft:seagrass", nil)
if !ok || !r.canSeagrassSurvive(position.X, position.Y, position.Z, set) {
return false
}
return r.setBlock(position.X, position.Y, position.Z, short)
}
func (r *decorationRegion) canSeagrassSurvive(x, y, z int, set *worldgen.FeatureSet) bool {
below := r.getBlock(x, y-1, z)
state, ok := stateByID(below)
return ok && fullSolidState(below) && !blockTagContains(set, "minecraft:cannot_support_seagrass", state.Name)
}
func blockTagContains(set *worldgen.FeatureSet, tag, name string) bool {
for _, member := range flattenBlockTag(set, tag, nil) {
if member == name {
return true
}
}
return false
}
func (r *decorationRegion) placeVegetationPatch(random worldgen.RandomSource, origin worldgen.FeaturePosition, config worldgen.VegetationPatchFeatureConfig, set *worldgen.FeatureSet) bool {
radiusX := config.XZRadiusMin + 1
if config.XZRadiusMax > config.XZRadiusMin {
radiusX += int(random.NextIntN(int32(config.XZRadiusMax - config.XZRadiusMin + 1)))
}
radiusZ := config.XZRadiusMin + 1
if config.XZRadiusMax > config.XZRadiusMin {
radiusZ += int(random.NextIntN(int32(config.XZRadiusMax - config.XZRadiusMin + 1)))
}
direction := -1
if config.Surface == "ceiling" {
direction = 1
}
replaceable := geodeTagIDs(set, config.ReplaceableTag)
ground, groundOK := nameToStateID(config.Ground.Name, config.Ground.Properties)
if !groundOK {
return false
}
placed := false
var groundPositions [][3]int
for dx := -radiusX; dx <= radiusX; dx++ {
for dz := -radiusZ; dz <= radiusZ; dz++ {
onXEdge := dx == -radiusX || dx == radiusX
onZEdge := dz == -radiusZ || dz == radiusZ
if onXEdge && onZEdge {
continue
}
if (onXEdge || onZEdge) && random.NextFloat() >= config.ExtraEdgeColumnChance {
continue
}
p := origin
p.X += dx
p.Z += dz
current := r.getBlock(p.X, p.Y, p.Z)
steps := 0
for current == StateAir && steps < config.VerticalRange {
p.Y += direction
current = r.getBlock(p.X, p.Y, p.Z)
steps++
}
steps = 0
for current != StateAir && steps < config.VerticalRange {
p.Y -= direction
current = r.getBlock(p.X, p.Y, p.Z)
steps++
}
// Vanilla requires the candidate cell to be empty and the adjacent
// surface block to expose a sturdy face toward the patch.
if r.getBlock(p.X, p.Y, p.Z) != StateAir {
continue
}
groundPos := p
groundPos.Y += direction
if !fullSolidState(r.getBlock(groundPos.X, groundPos.Y, groundPos.Z)) {
continue
}
depth := config.DepthMin
if config.DepthMax > config.DepthMin {
depth += int(random.NextIntN(int32(config.DepthMax - config.DepthMin + 1)))
}
if config.ExtraBottomBlockChance > 0 && random.NextFloat() < config.ExtraBottomBlockChance {
depth++
}
columnPlaced := false
for i := 0; i < depth; i++ {
gx, gy, gz := groundPos.X, groundPos.Y+direction*i, groundPos.Z
if !replaceable[r.getBlock(gx, gy, gz)] {
break
}
if r.setBlock(gx, gy, gz, ground) {
placed = true
columnPlaced = true
}
}
if columnPlaced {
groundPositions = append(groundPositions, [3]int{p.X, p.Y, p.Z})
}
}
}
// Keep the confirmed RNG contract while the nested placed-feature pass is
// independently validated against the full fixture.
for range groundPositions {
if config.VegetationChance > 0 {
random.NextFloat()
}
}
return placed
}
// placeSimpleBlockFeature implements the simple_block feature variants used
// by the moss vegetation patches. State-provider selection is weighted and
// consumes the same feature RNG that the configured feature receives.
func (r *decorationRegion) placeSimpleBlockFeature(random worldgen.RandomSource, position worldgen.FeaturePosition, config worldgen.SimpleBlockFeatureConfig, set *worldgen.FeatureSet) bool {
total := 0
for _, entry := range config.States {
if entry.Weight > 0 {
total += entry.Weight
}
}
if total <= 0 || r.getBlock(position.X, position.Y, position.Z) != StateAir {
return false
}
roll := int(random.NextIntN(int32(total)))
chosen := worldgen.BlockState{}
for _, entry := range config.States {
if entry.Weight <= 0 {
continue
}
if roll < entry.Weight {
chosen = entry.State
break
}
roll -= entry.Weight
}
state, ok := nameToStateID(chosen.Name, chosen.Properties)
if !ok || !r.canVegetationSurvive(position, chosen.Name, set) {
return false
}
if chosen.Name == "minecraft:tall_grass" && chosen.Properties["half"] == "lower" {
upper, upperOK := nameToStateID(chosen.Name, map[string]string{"half": "upper"})
if !upperOK || r.getBlock(position.X, position.Y+1, position.Z) != StateAir {
return false
}
if !r.setBlock(position.X, position.Y, position.Z, state) {
return false
}
return r.setBlock(position.X, position.Y+1, position.Z, upper)
}
return r.setBlock(position.X, position.Y, position.Z, state)
}
func (r *decorationRegion) canVegetationSurvive(position worldgen.FeaturePosition, name string, set *worldgen.FeatureSet) bool {
if position.Y <= MinY {
return false
}
below := r.getBlock(position.X, position.Y-1, position.Z)
if name == "minecraft:moss_carpet" || name == "minecraft:pale_moss_carpet" {
return fullSolidState(below)
}
for _, supported := range flattenBlockTag(set, "minecraft:supports_vegetation", nil) {
if state, ok := stateByID(below); ok && state.Name == supported {
return true
}
}
return false
}