world: replay datapack features across regions
This commit is contained in:
parent
f1d1d109f4
commit
d8b5d0f79b
13 changed files with 1259 additions and 8 deletions
|
|
@ -28,6 +28,9 @@ func (r *decorationRegion) testBlockPredicate(set *worldgen.FeatureSet, raw json
|
|||
switch predicate.Type {
|
||||
case "minecraft:true":
|
||||
return true, nil
|
||||
case "minecraft:solid":
|
||||
state := r.getBlock(position.X, position.Y, position.Z)
|
||||
return state != StateAir && stateFlags(state)&flagBlocksMotion != 0, nil
|
||||
case "minecraft:inside_world_bounds":
|
||||
return position.Y >= MinY && position.Y < MinY+WorldHeight, nil
|
||||
case "minecraft:matching_blocks":
|
||||
|
|
|
|||
105
internal/world/disks.go
Normal file
105
internal/world/disks.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package world
|
||||
|
||||
import "regionio/internal/worldgen"
|
||||
|
||||
// placeScheduledDisks replays the vanilla disk features from one source
|
||||
// center into the mutable decoration region. Disks are in the underground-ore
|
||||
// feature stage (6), after the ore entries in the 26.1.2 overworld datapack.
|
||||
func (r *decorationRegion) placeScheduledDisks(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(), undergroundOresStage)
|
||||
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 || configured.Type != "minecraft:disk" {
|
||||
continue
|
||||
}
|
||||
config, err := set.Disk(placed.Feature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
random.SetFeatureSeed(decorationSeed, scheduled.Index, undergroundOresStage)
|
||||
context := r.placementContext(func(position worldgen.FeaturePosition) bool {
|
||||
return r.biomeAllowsFeature(set, scheduled.Name, undergroundOresStage, position)
|
||||
})
|
||||
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
|
||||
return r.placeDisk(set, random, position, config)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *decorationRegion) placeDisk(set *worldgen.FeatureSet, random worldgen.RandomSource, position worldgen.FeaturePosition, config worldgen.DiskFeatureConfig) error {
|
||||
radius := config.RadiusMin
|
||||
if config.RadiusMax > config.RadiusMin {
|
||||
radius += int(random.NextIntN(int32(config.RadiusMax - config.RadiusMin + 1)))
|
||||
}
|
||||
fallback, ok := nameToStateID(config.Fallback.Name, config.Fallback.Properties)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
ruleStates := make([]uint16, len(config.Rules))
|
||||
for i, rule := range config.Rules {
|
||||
state, ok := nameToStateID(rule.Then.Name, rule.Then.Properties)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
ruleStates[i] = state
|
||||
}
|
||||
targets := make(map[uint16]bool, len(config.Targets))
|
||||
for _, name := range config.Targets {
|
||||
if id, ok := nameToStateID(name, nil); ok {
|
||||
targets[id] = true
|
||||
}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
// DiskFeature walks BlockPos.betweenClosed: X is the innermost coordinate,
|
||||
// then Y, then Z. Each disk column is therefore placed top-down before the
|
||||
// next column is visited. This is observable for rule-based providers whose
|
||||
// predicate inspects the just-updated block above.
|
||||
for dz := -radius; dz <= radius; dz++ {
|
||||
for dx := -radius; dx <= radius; dx++ {
|
||||
if dx*dx+dz*dz > radius*radius {
|
||||
continue
|
||||
}
|
||||
for y := position.Y + config.HalfHeight; y >= position.Y-config.HalfHeight; y-- {
|
||||
x, z := position.X+dx, position.Z+dz
|
||||
current := r.getBlock(x, y, z)
|
||||
if !targets[current] {
|
||||
continue
|
||||
}
|
||||
placeState := fallback
|
||||
for i, rule := range config.Rules {
|
||||
matched, err := r.testBlockPredicate(set, rule.IfTrue, worldgen.FeaturePosition{X: x, Y: y, Z: z})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if matched {
|
||||
placeState = ruleStates[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
r.setBlock(x, y, z, placeState)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
91
internal/world/disks_test.go
Normal file
91
internal/world/disks_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"regionio/internal/worldgen"
|
||||
)
|
||||
|
||||
func TestDiskStateProviderRules(t *testing.T) {
|
||||
set, err := worldgen.LoadFeatureSet()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
makeRegion := func(block uint16) *decorationRegion {
|
||||
chunk := NewChunk(0, 0, BiomePlains)
|
||||
chunk.SetBlock(8, 0, 8, block)
|
||||
region, err := newDecorationRegion([]*Chunk{chunk})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := region.setSource(0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return region
|
||||
}
|
||||
|
||||
dirt, ok := nameToStateID("minecraft:dirt", nil)
|
||||
if !ok {
|
||||
t.Fatal("dirt state missing")
|
||||
}
|
||||
|
||||
sandConfig, err := set.Disk("minecraft:disk_sand")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sandConfig.RadiusMin, sandConfig.RadiusMax, sandConfig.HalfHeight = 0, 0, 0
|
||||
region := makeRegion(dirt)
|
||||
if err := region.placeDisk(set, worldgen.NewWorldgenRandom(1), worldgen.FeaturePosition{X: 8, Y: 0, Z: 8}, sandConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sandstone, ok := nameToStateID("minecraft:sandstone", nil)
|
||||
if !ok || region.getBlock(8, 0, 8) != sandstone {
|
||||
t.Fatalf("sand rule state = %d, want sandstone %d", region.getBlock(8, 0, 8), sandstone)
|
||||
}
|
||||
|
||||
grassConfig, err := set.Disk("minecraft:disk_grass")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
grassConfig.RadiusMin, grassConfig.RadiusMax, grassConfig.HalfHeight = 0, 0, 0
|
||||
region = makeRegion(dirt)
|
||||
if err := region.placeDisk(set, worldgen.NewWorldgenRandom(1), worldgen.FeaturePosition{X: 8, Y: 0, Z: 8}, grassConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
grass, ok := nameToStateID("minecraft:grass_block", map[string]string{"snowy": "false"})
|
||||
if !ok || region.getBlock(8, 0, 8) != grass {
|
||||
t.Fatalf("grass rule state = %d, want grass %d", region.getBlock(8, 0, 8), grass)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskStateProviderFallback(t *testing.T) {
|
||||
set, err := worldgen.LoadFeatureSet()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := set.Disk("minecraft:disk_grass")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.RadiusMin, config.RadiusMax, config.HalfHeight = 0, 0, 0
|
||||
|
||||
dirt, _ := nameToStateID("minecraft:dirt", nil)
|
||||
stone, _ := nameToStateID("minecraft:stone", nil)
|
||||
chunk := NewChunk(0, 0, BiomePlains)
|
||||
chunk.SetBlock(8, 0, 8, dirt)
|
||||
chunk.SetBlock(8, 1, 8, stone)
|
||||
region, err := newDecorationRegion([]*Chunk{chunk})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := region.setSource(0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := region.placeDisk(set, worldgen.NewWorldgenRandom(1), worldgen.FeaturePosition{X: 8, Y: 0, Z: 8}, config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := region.getBlock(8, 0, 8); got != dirt {
|
||||
t.Fatalf("fallback state = %d, want dirt %d", got, dirt)
|
||||
}
|
||||
}
|
||||
|
|
@ -33,8 +33,14 @@ func (r *decorationRegion) replayScheduledOres(seed int64, targetX, targetZ int3
|
|||
if err := r.setSource(source.X, source.Z); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.placeScheduledOres(seed); err != nil {
|
||||
return fmt.Errorf("world: replay source (%d,%d): %w", source.X, source.Z, err)
|
||||
if err := r.placeScheduledGeodes(seed); err != nil {
|
||||
return fmt.Errorf("world: replay source geodes (%d,%d): %w", source.X, source.Z, err)
|
||||
}
|
||||
if err := r.placeScheduledUndergroundOresStage(seed); err != nil {
|
||||
return fmt.Errorf("world: replay source underground ores (%d,%d): %w", source.X, source.Z, err)
|
||||
}
|
||||
if err := r.placeScheduledVegetationPatches(seed); err != nil {
|
||||
return fmt.Errorf("world: replay source vegetation patches (%d,%d): %w", source.X, source.Z, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
242
internal/world/geodes.go
Normal file
242
internal/world/geodes.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"regionio/internal/worldgen"
|
||||
)
|
||||
|
||||
const geodesStage = 2
|
||||
|
||||
type geodePoint struct {
|
||||
x, y, z int
|
||||
offset int
|
||||
}
|
||||
|
||||
// placeScheduledGeodes replays the stage-2 amethyst geode feature. The layer
|
||||
// calculation follows GeodeFeature.place: sampled distance points are combined
|
||||
// with vanilla normal-noise perturbation, then the nearest layer threshold
|
||||
// selects filling, inner, middle, or outer material.
|
||||
func (r *decorationRegion) placeScheduledGeodes(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(), geodesStage)
|
||||
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 || configured.Type != "minecraft:geode" {
|
||||
continue
|
||||
}
|
||||
config, err := set.Geode(placed.Feature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
random.SetFeatureSeed(decorationSeed, scheduled.Index, geodesStage)
|
||||
context := r.placementContext(func(position worldgen.FeaturePosition) bool {
|
||||
return r.biomeAllowsFeature(set, scheduled.Name, geodesStage, position)
|
||||
})
|
||||
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
|
||||
r.placeGeode(random, seed, position, config, set)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *decorationRegion) placeGeode(random worldgen.RandomSource, seed int64, origin worldgen.FeaturePosition, config worldgen.GeodeFeatureConfig, set *worldgen.FeatureSet) bool {
|
||||
distributionPoints := sampleGeodeInt(random, config.DistributionMin, config.DistributionMax)
|
||||
pointScale := float64(distributionPoints) / float64(config.OuterWallMax)
|
||||
fillingThreshold := 1 / math.Sqrt(config.FillingLayer)
|
||||
innerThreshold := 1 / math.Sqrt(config.InnerLayer+pointScale)
|
||||
middleThreshold := 1 / math.Sqrt(config.MiddleLayer+pointScale)
|
||||
outerThreshold := 1 / math.Sqrt(config.OuterLayer+pointScale)
|
||||
crackSize := config.BaseCrackSize + random.NextDouble()/2
|
||||
if distributionPoints > 3 {
|
||||
crackSize += pointScale
|
||||
}
|
||||
crackThreshold := 1 / math.Sqrt(crackSize)
|
||||
generateCrack := random.NextFloat() < float32(config.CrackChance)
|
||||
|
||||
points := make([]geodePoint, 0, distributionPoints)
|
||||
invalid := 0
|
||||
invalidBlocks := geodeTagIDs(set, config.InvalidBlocksTag)
|
||||
for i := 0; i < distributionPoints; i++ {
|
||||
point := geodePoint{
|
||||
x: origin.X + sampleGeodeInt(random, config.OuterWallMin, config.OuterWallMax),
|
||||
y: origin.Y + sampleGeodeInt(random, config.OuterWallMin, config.OuterWallMax),
|
||||
z: origin.Z + sampleGeodeInt(random, config.OuterWallMin, config.OuterWallMax),
|
||||
offset: sampleGeodeInt(random, config.PointOffsetMin, config.PointOffsetMax),
|
||||
}
|
||||
state := r.getBlock(point.x, point.y, point.z)
|
||||
if state == StateAir || invalidBlocks[state] {
|
||||
invalid++
|
||||
if invalid > config.InvalidBlocksThreshold {
|
||||
return false
|
||||
}
|
||||
}
|
||||
points = append(points, point)
|
||||
}
|
||||
|
||||
crackPoints := geodeCrackPoints(origin, random, distributionPoints, generateCrack)
|
||||
noise := worldgen.NewNormalNoise(worldgen.NewLegacy(seed), -4, []float64{1})
|
||||
cannotReplace := geodeTagIDs(set, config.CannotReplaceTag)
|
||||
placed := false
|
||||
var potentialPlacements [][3]int
|
||||
for z := origin.Z + config.MinGenOffset; z <= origin.Z+config.MaxGenOffset; z++ {
|
||||
for y := origin.Y + config.MinGenOffset; y <= origin.Y+config.MaxGenOffset; y++ {
|
||||
for x := origin.X + config.MinGenOffset; x <= origin.X+config.MaxGenOffset; x++ {
|
||||
perturbation := noise.GetValue(float64(x), float64(y), float64(z)) * config.NoiseMultiplier
|
||||
innerDistance := geodeDistance(x, y, z, points, perturbation)
|
||||
if innerDistance < outerThreshold {
|
||||
continue
|
||||
}
|
||||
current := r.getBlock(x, y, z)
|
||||
if cannotReplace[current] {
|
||||
continue
|
||||
}
|
||||
crackDistance := 0.0
|
||||
if generateCrack {
|
||||
crackDistance = geodeDistanceWithOffset(x, y, z, crackPoints, config.CrackPointOffset, perturbation)
|
||||
}
|
||||
var state uint16
|
||||
switch {
|
||||
case generateCrack && crackDistance >= crackThreshold && innerDistance < fillingThreshold:
|
||||
state = StateAir
|
||||
case innerDistance >= fillingThreshold:
|
||||
state, _ = nameToStateID(config.Filling.Name, config.Filling.Properties)
|
||||
case innerDistance >= innerThreshold:
|
||||
alternate := random.NextFloat() < float32(config.UseAlternateLayerChance)
|
||||
provider := config.Inner
|
||||
if alternate && config.AlternateInner.Name != "" {
|
||||
provider = config.AlternateInner
|
||||
}
|
||||
state, _ = nameToStateID(provider.Name, provider.Properties)
|
||||
if (!config.PlacementsRequireAlternate || alternate) &&
|
||||
random.NextFloat() < float32(config.UsePotentialPlacementsChance) {
|
||||
potentialPlacements = append(potentialPlacements, [3]int{x, y, z})
|
||||
}
|
||||
case innerDistance >= middleThreshold:
|
||||
state, _ = nameToStateID(config.Middle.Name, config.Middle.Properties)
|
||||
case innerDistance >= outerThreshold:
|
||||
state, _ = nameToStateID(config.Outer.Name, config.Outer.Properties)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if r.setBlock(x, y, z, state) {
|
||||
placed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
r.placeGeodeInnerPlacements(random, potentialPlacements, config, cannotReplace)
|
||||
return placed
|
||||
}
|
||||
|
||||
func (r *decorationRegion) placeGeodeInnerPlacements(random worldgen.RandomSource, positions [][3]int, config worldgen.GeodeFeatureConfig, cannotReplace map[uint16]bool) {
|
||||
if len(config.InnerPlacements) == 0 {
|
||||
return
|
||||
}
|
||||
directions := [...]struct {
|
||||
dx, dy, dz int
|
||||
name string
|
||||
}{
|
||||
{0, -1, 0, "down"}, {0, 1, 0, "up"}, {0, 0, -1, "north"},
|
||||
{0, 0, 1, "south"}, {-1, 0, 0, "west"}, {1, 0, 0, "east"},
|
||||
}
|
||||
for _, position := range positions {
|
||||
placement := config.InnerPlacements[int(random.NextIntN(int32(len(config.InnerPlacements))))]
|
||||
for _, direction := range directions {
|
||||
x, y, z := position[0]+direction.dx, position[1]+direction.dy, position[2]+direction.dz
|
||||
current := r.getBlock(x, y, z)
|
||||
if current != StateAir && !isWaterState(current) {
|
||||
continue
|
||||
}
|
||||
props := make(map[string]string, len(placement.Properties))
|
||||
for key, value := range placement.Properties {
|
||||
props[key] = value
|
||||
}
|
||||
props["facing"] = direction.name
|
||||
props["waterlogged"] = "false"
|
||||
if isWaterState(current) {
|
||||
props["waterlogged"] = "true"
|
||||
}
|
||||
state, ok := nameToStateID(placement.Name, props)
|
||||
if ok && !cannotReplace[current] {
|
||||
if r.setBlock(x, y, z, state) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sampleGeodeInt(random worldgen.RandomSource, min, max int) int {
|
||||
if max <= min {
|
||||
return min
|
||||
}
|
||||
return min + int(random.NextIntN(int32(max-min+1)))
|
||||
}
|
||||
|
||||
func geodeTagIDs(set *worldgen.FeatureSet, tag string) map[uint16]bool {
|
||||
if strings.HasPrefix(tag, "#") {
|
||||
tag = tag[1:]
|
||||
}
|
||||
ids := make(map[uint16]bool)
|
||||
for _, name := range flattenBlockTag(set, tag, nil) {
|
||||
if id, ok := nameToStateID(name, nil); ok {
|
||||
ids[id] = true
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func geodeDistance(x, y, z int, points []geodePoint, perturbation float64) float64 {
|
||||
distance := 0.0
|
||||
for _, point := range points {
|
||||
dx, dy, dz := x-point.x, y-point.y, z-point.z
|
||||
distance += 1/math.Sqrt(float64(dx*dx+dy*dy+dz*dz+point.offset)) + perturbation
|
||||
}
|
||||
return distance
|
||||
}
|
||||
|
||||
func geodeDistanceWithOffset(x, y, z int, points []geodePoint, offset int, perturbation float64) float64 {
|
||||
distance := 0.0
|
||||
for _, point := range points {
|
||||
dx, dy, dz := x-point.x, y-point.y, z-point.z
|
||||
distance += 1/math.Sqrt(float64(dx*dx+dy*dy+dz*dz+offset)) + perturbation
|
||||
}
|
||||
return distance
|
||||
}
|
||||
|
||||
func geodeCrackPoints(origin worldgen.FeaturePosition, random worldgen.RandomSource, distributionPoints int, enabled bool) []geodePoint {
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
offset := distributionPoints*2 + 1
|
||||
switch random.NextIntN(4) {
|
||||
case 0:
|
||||
return []geodePoint{{x: origin.X + offset, y: origin.Y + 7, z: origin.Z}, {x: origin.X + offset, y: origin.Y + 5, z: origin.Z}, {x: origin.X + offset, y: origin.Y + 1, z: origin.Z}}
|
||||
case 1:
|
||||
return []geodePoint{{x: origin.X, y: origin.Y + 7, z: origin.Z + offset}, {x: origin.X, y: origin.Y + 5, z: origin.Z + offset}, {x: origin.X, y: origin.Y + 1, z: origin.Z + offset}}
|
||||
case 2:
|
||||
return []geodePoint{{x: origin.X + offset, y: origin.Y + 7, z: origin.Z + offset}, {x: origin.X + offset, y: origin.Y + 5, z: origin.Z + offset}, {x: origin.X + offset, y: origin.Y + 1, z: origin.Z + offset}}
|
||||
default:
|
||||
return []geodePoint{{x: origin.X, y: origin.Y + 7, z: origin.Z}, {x: origin.X, y: origin.Y + 5, z: origin.Z}, {x: origin.X, y: origin.Y + 1, z: origin.Z}}
|
||||
}
|
||||
}
|
||||
76
internal/world/geodes_test.go
Normal file
76
internal/world/geodes_test.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"regionio/internal/worldgen"
|
||||
)
|
||||
|
||||
func TestGeodePlacementIsDeterministicAndLayered(t *testing.T) {
|
||||
set, err := worldgen.LoadFeatureSet()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := set.Geode("minecraft:amethyst_geode")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
makeRegion := func() *decorationRegion {
|
||||
chunks := make([]*Chunk, 0, 25)
|
||||
for cx := int32(-2); cx <= 2; cx++ {
|
||||
for cz := int32(-2); cz <= 2; cz++ {
|
||||
chunk := NewChunk(cx, cz, BiomePlains)
|
||||
for y := MinY; y < MinY+WorldHeight; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
chunk.setBlockRaw(x, y, z, StateStone)
|
||||
}
|
||||
}
|
||||
}
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
}
|
||||
region, err := newDecorationRegion(chunks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := region.setSource(0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return region
|
||||
}
|
||||
origin := worldgen.FeaturePosition{X: 8, Y: 0, Z: 8}
|
||||
a, b := makeRegion(), makeRegion()
|
||||
if !a.placeGeode(worldgen.NewWorldgenRandom(42), 12345, origin, config, set) {
|
||||
t.Fatal("geode placement changed no blocks")
|
||||
}
|
||||
if !b.placeGeode(worldgen.NewWorldgenRandom(42), 12345, origin, config, set) {
|
||||
t.Fatal("second geode placement changed no blocks")
|
||||
}
|
||||
counts := map[uint16]int{}
|
||||
for cx := int32(-1); cx <= 1; cx++ {
|
||||
for cz := int32(-1); cz <= 1; cz++ {
|
||||
left := a.chunks[[2]int32{cx, cz}]
|
||||
right := b.chunks[[2]int32{cx, cz}]
|
||||
for y := MinY; y < MinY+WorldHeight; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
if left.GetBlock(x, y, z) != right.GetBlock(x, y, z) {
|
||||
t.Fatalf("geodes differ at chunk (%d,%d) block (%d,%d,%d)", cx, cz, x, y, z)
|
||||
}
|
||||
state := left.GetBlock(x, y, z)
|
||||
if state != StateStone {
|
||||
counts[state]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"minecraft:smooth_basalt", "minecraft:calcite", "minecraft:amethyst_block"} {
|
||||
state, ok := nameToStateID(name, nil)
|
||||
if !ok || counts[state] == 0 {
|
||||
t.Fatalf("geode did not place %s: counts=%v", name, counts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -135,10 +135,14 @@ func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY,
|
|||
func buildOreSpheres(random worldgen.RandomSource, originX, originY, originZ, size int) []oreSphere {
|
||||
angle := random.NextFloat() * float32(math.Pi)
|
||||
extent := float32(size) / 8.0
|
||||
x0 := float64(originX) + float64(worldgen.MthSin(float64(angle))*extent)
|
||||
x1 := float64(originX) - float64(worldgen.MthSin(float64(angle))*extent)
|
||||
z0 := float64(originZ) + float64(worldgen.MthCos(float64(angle))*extent)
|
||||
z1 := float64(originZ) - float64(worldgen.MthCos(float64(angle))*extent)
|
||||
// OreFeature.place uses java.lang.Math for the vein axis. The later radius
|
||||
// wave uses Mth.sin, but using the lookup table here shifts every sphere.
|
||||
xOffset := math.Sin(float64(angle)) * float64(extent)
|
||||
zOffset := math.Cos(float64(angle)) * float64(extent)
|
||||
x0 := float64(originX) + xOffset
|
||||
x1 := float64(originX) - xOffset
|
||||
z0 := float64(originZ) + zOffset
|
||||
z1 := float64(originZ) - zOffset
|
||||
y0 := float64(originY + int(random.NextIntN(3)) - 2)
|
||||
y1 := float64(originY + int(random.NextIntN(3)) - 2)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,84 @@ package world
|
|||
|
||||
import "regionio/internal/worldgen"
|
||||
|
||||
// placeScheduledUndergroundOresStage executes the complete vanilla stage-6
|
||||
// schedule in one pass. Feature seeds are independent, but feature writes are
|
||||
// not: underwater magma is ordered between copper and clay, followed by the
|
||||
// disk features. Replaying by feature type silently changed the terrain seen
|
||||
// by later entries.
|
||||
func (r *decorationRegion) placeScheduledUndergroundOresStage(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(), undergroundOresStage)
|
||||
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}
|
||||
magma, magmaOK := nameToStateID("minecraft:magma_block", nil)
|
||||
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, undergroundOresStage)
|
||||
context := r.placementContext(func(position worldgen.FeaturePosition) bool {
|
||||
return r.biomeAllowsFeature(set, scheduled.Name, undergroundOresStage, position)
|
||||
})
|
||||
switch configured.Type {
|
||||
case "minecraft:ore":
|
||||
config, err := set.Ore(placed.Feature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targets, ok := resolveOreTargets(set, config)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
|
||||
placeOreEllipsoidRegion(r, random, position.X, position.Y, position.Z, config.Size, config.DiscardAirExposure, targets)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case "minecraft:underwater_magma":
|
||||
if !magmaOK {
|
||||
continue
|
||||
}
|
||||
config, err := set.UnderwaterMagma(placed.Feature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
|
||||
r.placeUnderwaterMagma(random, position, config, magma)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case "minecraft:disk":
|
||||
config, err := set.Disk(placed.Feature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
|
||||
return r.placeDisk(set, random, position, config)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *decorationRegion) placeScheduledOres(seed int64) error {
|
||||
return r.placeScheduledOresWithOrder(seed, possibleBiomeOrder(), 0)
|
||||
}
|
||||
|
|
|
|||
111
internal/world/underwater_magma.go
Normal file
111
internal/world/underwater_magma.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package world
|
||||
|
||||
import "regionio/internal/worldgen"
|
||||
|
||||
func (r *decorationRegion) placeScheduledUnderwaterMagma(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(), undergroundOresStage)
|
||||
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 || configured.Type != "minecraft:underwater_magma" {
|
||||
continue
|
||||
}
|
||||
config, err := set.UnderwaterMagma(placed.Feature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
magma, ok := nameToStateID("minecraft:magma_block", nil)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
random.SetFeatureSeed(decorationSeed, scheduled.Index, undergroundOresStage)
|
||||
context := r.placementContext(func(position worldgen.FeaturePosition) bool {
|
||||
return r.biomeAllowsFeature(set, scheduled.Name, undergroundOresStage, position)
|
||||
})
|
||||
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
|
||||
r.placeUnderwaterMagma(random, position, config, magma)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *decorationRegion) placeUnderwaterMagma(random worldgen.RandomSource, origin worldgen.FeaturePosition, config worldgen.UnderwaterMagmaFeatureConfig, magma uint16) bool {
|
||||
floorY, ok := r.underwaterFloor(origin.X, origin.Y, origin.Z, config.FloorSearchRange)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
placed := false
|
||||
radius := config.PlacementRadiusAroundFloor
|
||||
for x := origin.X - radius; x <= origin.X+radius; x++ {
|
||||
for y := floorY - radius; y <= floorY+radius; y++ {
|
||||
for z := origin.Z - radius; z <= origin.Z+radius; z++ {
|
||||
if random.NextFloat() >= config.PlacementProbability || !r.validUnderwaterMagmaPosition(x, y, z) {
|
||||
continue
|
||||
}
|
||||
if r.setBlock(x, y, z, magma) {
|
||||
placed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
func (r *decorationRegion) underwaterFloor(x, y, z, search int) (int, bool) {
|
||||
if y < MinY || y >= MinY+WorldHeight {
|
||||
return 0, false
|
||||
}
|
||||
isWater := func(value uint16) bool { return isWaterState(value) }
|
||||
if !isWater(r.getBlock(x, y, z)) {
|
||||
return 0, false
|
||||
}
|
||||
// Column.scan checks the starting water block, then moves at most
|
||||
// search-1 blocks in each direction before testing the terminating state.
|
||||
for step := 1; step < search; step++ {
|
||||
y--
|
||||
if y < MinY {
|
||||
return 0, false
|
||||
}
|
||||
if !isWater(r.getBlock(x, y, z)) {
|
||||
return y, true
|
||||
}
|
||||
}
|
||||
if !isWater(r.getBlock(x, y, z)) {
|
||||
return y, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (r *decorationRegion) validUnderwaterMagmaPosition(x, y, z int) bool {
|
||||
if !fullSolidState(r.getBlock(x, y, z)) {
|
||||
return false
|
||||
}
|
||||
for _, offset := range [][3]int{{0, -1, 0}, {-1, 0, 0}, {1, 0, 0}, {0, 0, -1}, {0, 0, 1}} {
|
||||
if !fullSolidState(r.getBlock(x+offset[0], y+offset[1], z+offset[2])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func fullSolidState(state uint16) bool {
|
||||
return state != StateAir && !isWaterState(state) && !isLavaState(state) && stateFlags(state)&flagBlocksMotion != 0
|
||||
}
|
||||
73
internal/world/underwater_magma_test.go
Normal file
73
internal/world/underwater_magma_test.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"regionio/internal/worldgen"
|
||||
)
|
||||
|
||||
func TestUnderwaterFloorMatchesColumnScanRange(t *testing.T) {
|
||||
chunk := NewChunk(0, 0, BiomePlains)
|
||||
region, err := newDecorationRegion([]*Chunk{chunk})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for y := 7; y <= 10; y++ {
|
||||
chunk.SetBlock(3, y, 4, StateWater)
|
||||
}
|
||||
chunk.SetBlock(3, 6, 4, StateStone)
|
||||
if floor, ok := region.underwaterFloor(3, 10, 4, 5); !ok || floor != 6 {
|
||||
t.Fatalf("floor = %d, %v; want 6, true", floor, ok)
|
||||
}
|
||||
if _, ok := region.underwaterFloor(3, 10, 4, 4); ok {
|
||||
t.Fatal("floor outside Column.scan range was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderwaterMagmaPlacementIsDeterministic(t *testing.T) {
|
||||
makeRegion := func() *decorationRegion {
|
||||
chunk := NewChunk(0, 0, BiomePlains)
|
||||
for x := 6; x <= 10; x++ {
|
||||
for z := 6; z <= 10; z++ {
|
||||
for y := 0; y <= 5; y++ {
|
||||
chunk.SetBlock(x, y, z, StateStone)
|
||||
}
|
||||
for y := 6; y <= 10; y++ {
|
||||
chunk.SetBlock(x, y, z, StateWater)
|
||||
}
|
||||
}
|
||||
}
|
||||
region, err := newDecorationRegion([]*Chunk{chunk})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := region.setSource(0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return region
|
||||
}
|
||||
config := worldgen.UnderwaterMagmaFeatureConfig{
|
||||
FloorSearchRange: 6, PlacementProbability: 1, PlacementRadiusAroundFloor: 1,
|
||||
}
|
||||
magma, ok := nameToStateID("minecraft:magma_block", nil)
|
||||
if !ok {
|
||||
t.Fatal("magma block state missing")
|
||||
}
|
||||
a, b := makeRegion(), makeRegion()
|
||||
origin := worldgen.FeaturePosition{X: 8, Y: 10, Z: 8}
|
||||
if !a.placeUnderwaterMagma(worldgen.NewWorldgenRandom(42), origin, config, magma) {
|
||||
t.Fatal("magma placement changed no blocks")
|
||||
}
|
||||
if !b.placeUnderwaterMagma(worldgen.NewWorldgenRandom(42), origin, config, magma) {
|
||||
t.Fatal("second magma placement changed no blocks")
|
||||
}
|
||||
for x := 6; x <= 10; x++ {
|
||||
for z := 6; z <= 10; z++ {
|
||||
for y := 0; y <= 10; y++ {
|
||||
if a.getBlock(x, y, z) != b.getBlock(x, y, z) {
|
||||
t.Fatalf("placements differ at (%d,%d,%d)", x, y, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
150
internal/world/vegetation_patches_test.go
Normal file
150
internal/world/vegetation_patches_test.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"regionio/internal/worldgen"
|
||||
)
|
||||
|
||||
func TestVegetationPatchPlacesFloorGroundDeterministically(t *testing.T) {
|
||||
set, err := worldgen.LoadFeatureSet()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := set.VegetationPatch("minecraft:moss_patch")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.XZRadiusMin, config.XZRadiusMax = 1, 1
|
||||
config.ExtraEdgeColumnChance = 1
|
||||
config.VegetationChance = 0
|
||||
|
||||
makeRegion := func() *decorationRegion {
|
||||
chunk := NewChunk(0, 0, BiomePlains)
|
||||
dirt, ok := nameToStateID("minecraft:dirt", nil)
|
||||
if !ok {
|
||||
t.Fatal("dirt state missing")
|
||||
}
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
chunk.SetBlock(x, 0, z, dirt)
|
||||
}
|
||||
}
|
||||
region, err := newDecorationRegion([]*Chunk{chunk})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := region.setSource(0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return region
|
||||
}
|
||||
|
||||
a, b := makeRegion(), makeRegion()
|
||||
origin := worldgen.FeaturePosition{X: 8, Y: 5, Z: 8}
|
||||
if !a.placeVegetationPatch(worldgen.NewWorldgenRandom(123), origin, config, set) {
|
||||
t.Fatal("patch changed no blocks")
|
||||
}
|
||||
if !b.placeVegetationPatch(worldgen.NewWorldgenRandom(123), origin, config, set) {
|
||||
t.Fatal("second patch changed no blocks")
|
||||
}
|
||||
moss, ok := nameToStateID("minecraft:moss_block", nil)
|
||||
if !ok || a.getBlock(8, 0, 8) != moss {
|
||||
t.Fatalf("center ground = %d, want moss %d", a.getBlock(8, 0, 8), moss)
|
||||
}
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
for y := 0; y < 6; y++ {
|
||||
if a.getBlock(x, y, z) != b.getBlock(x, y, z) {
|
||||
t.Fatalf("patches differ at (%d,%d,%d)", x, y, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleBlockFeatureWeightedProviderAndTallGrass(t *testing.T) {
|
||||
set, err := worldgen.LoadFeatureSet()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := set.SimpleBlock("minecraft:moss_vegetation")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.States = []worldgen.WeightedBlockState{{State: worldgen.BlockState{Name: "minecraft:tall_grass", Properties: map[string]string{"half": "lower"}}, Weight: 1}}
|
||||
chunk := NewChunk(0, 0, BiomePlains)
|
||||
moss, ok := nameToStateID("minecraft:moss_block", nil)
|
||||
if !ok {
|
||||
t.Fatal("moss state missing")
|
||||
}
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
chunk.SetBlock(x, 0, z, moss)
|
||||
}
|
||||
}
|
||||
region, err := newDecorationRegion([]*Chunk{chunk})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := region.setSource(0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
position := worldgen.FeaturePosition{X: 8, Y: 1, Z: 8}
|
||||
if !region.placeSimpleBlockFeature(worldgen.NewWorldgenRandom(7), position, config, set) {
|
||||
t.Fatal("simple block feature did not place")
|
||||
}
|
||||
lower, _ := nameToStateID("minecraft:tall_grass", map[string]string{"half": "lower"})
|
||||
upper, _ := nameToStateID("minecraft:tall_grass", map[string]string{"half": "upper"})
|
||||
if got := region.getBlock(8, 1, 8); got != lower {
|
||||
t.Fatalf("lower tall grass = %d, want %d", got, lower)
|
||||
}
|
||||
if got := region.getBlock(8, 2, 8); got != upper {
|
||||
t.Fatalf("upper tall grass = %d, want %d", got, upper)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAquaticVegetationUsesOceanFloor(t *testing.T) {
|
||||
set, err := worldgen.LoadFeatureSet()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
chunk := NewChunk(0, 0, BiomePlains)
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
chunk.SetBlock(x, 0, z, StateStone)
|
||||
for y := 1; y <= 12; y++ {
|
||||
chunk.SetBlock(x, y, z, StateWater)
|
||||
}
|
||||
}
|
||||
}
|
||||
region, err := newDecorationRegion([]*Chunk{chunk})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := region.setSource(0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !region.placeKelp(worldgen.NewWorldgenRandom(3), worldgen.FeaturePosition{X: 8, Y: MinY, Z: 8}, set) {
|
||||
t.Fatal("kelp did not place")
|
||||
}
|
||||
if got, ok := stateByID(region.getBlock(8, 1, 8)); !ok || got.Name != "minecraft:kelp_plant" && got.Name != "minecraft:kelp" {
|
||||
t.Fatalf("kelp floor state = %+v, %v", got, ok)
|
||||
}
|
||||
|
||||
if !region.placeSeagrass(worldgen.NewWorldgenRandom(9), worldgen.FeaturePosition{X: 8, Y: MinY, Z: 8}, 0, set) {
|
||||
t.Fatal("seagrass did not place")
|
||||
}
|
||||
found := false
|
||||
for x := 1; x < 16; x++ {
|
||||
for z := 1; z < 16; z++ {
|
||||
if state, ok := stateByID(region.getBlock(x, 1, z)); ok && state.Name == "minecraft:seagrass" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("short seagrass missing from ocean floor")
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import net.minecraft.util.valueproviders.ClampedNormalInt;
|
|||
import net.minecraft.util.valueproviders.TrapezoidInt;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import net.minecraft.world.level.levelgen.placement.CountPlacement;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacementModifier;
|
||||
import net.minecraft.world.level.levelgen.placement.RandomOffsetPlacement;
|
||||
|
|
@ -44,8 +45,14 @@ public final class VanillaPlacementVectors {
|
|||
WorldgenRandom decoration = new WorldgenRandom(new LegacyRandomSource(0L));
|
||||
long decorationSeed = decoration.setDecorationSeed(12345L, 0, 0);
|
||||
decoration.setFeatureSeed(decorationSeed, 10, 6);
|
||||
System.out.println("decoration.seed=" + decorationSeed);
|
||||
System.out.println("feature10.stage6=" + randomVector(decoration));
|
||||
System.out.println("legacy.decoration.seed=" + decorationSeed);
|
||||
System.out.println("legacy.feature10.stage6=" + randomVector(decoration));
|
||||
|
||||
WorldgenRandom xoroshiro = new WorldgenRandom(new XoroshiroRandomSource(0L));
|
||||
long xoroshiroSeed = xoroshiro.setDecorationSeed(12345L, 0, 0);
|
||||
xoroshiro.setFeatureSeed(xoroshiroSeed, 10, 6);
|
||||
System.out.println("xoroshiro.decoration.seed=" + xoroshiroSeed);
|
||||
System.out.println("xoroshiro.feature10.stage6=" + randomVector(xoroshiro));
|
||||
}
|
||||
|
||||
private static String randomVector(RandomSource random) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue