world: replay datapack features across regions
This commit is contained in:
parent
f1d1d109f4
commit
d8b5d0f79b
13 changed files with 1259 additions and 8 deletions
305
internal/world/vegetation_patches.go
Normal file
305
internal/world/vegetation_patches.go
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue