305 lines
10 KiB
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
|
|
}
|