world: replay datapack features across regions

This commit is contained in:
Daniar Mannanov 2026-08-25 03:59:10 +03:00
parent f1d1d109f4
commit d8b5d0f79b
13 changed files with 1259 additions and 8 deletions

View file

@ -28,6 +28,9 @@ func (r *decorationRegion) testBlockPredicate(set *worldgen.FeatureSet, raw json
switch predicate.Type { switch predicate.Type {
case "minecraft:true": case "minecraft:true":
return true, nil 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": case "minecraft:inside_world_bounds":
return position.Y >= MinY && position.Y < MinY+WorldHeight, nil return position.Y >= MinY && position.Y < MinY+WorldHeight, nil
case "minecraft:matching_blocks": case "minecraft:matching_blocks":

105
internal/world/disks.go Normal file
View 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
}

View 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)
}
}

View file

@ -33,8 +33,14 @@ func (r *decorationRegion) replayScheduledOres(seed int64, targetX, targetZ int3
if err := r.setSource(source.X, source.Z); err != nil { if err := r.setSource(source.X, source.Z); err != nil {
return err return err
} }
if err := r.placeScheduledOres(seed); err != nil { if err := r.placeScheduledGeodes(seed); err != nil {
return fmt.Errorf("world: replay source (%d,%d): %w", source.X, source.Z, err) 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 return nil

242
internal/world/geodes.go Normal file
View 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}}
}
}

View 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)
}
}
}

View file

@ -135,10 +135,14 @@ func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY,
func buildOreSpheres(random worldgen.RandomSource, originX, originY, originZ, size int) []oreSphere { func buildOreSpheres(random worldgen.RandomSource, originX, originY, originZ, size int) []oreSphere {
angle := random.NextFloat() * float32(math.Pi) angle := random.NextFloat() * float32(math.Pi)
extent := float32(size) / 8.0 extent := float32(size) / 8.0
x0 := float64(originX) + float64(worldgen.MthSin(float64(angle))*extent) // OreFeature.place uses java.lang.Math for the vein axis. The later radius
x1 := float64(originX) - float64(worldgen.MthSin(float64(angle))*extent) // wave uses Mth.sin, but using the lookup table here shifts every sphere.
z0 := float64(originZ) + float64(worldgen.MthCos(float64(angle))*extent) xOffset := math.Sin(float64(angle)) * float64(extent)
z1 := float64(originZ) - float64(worldgen.MthCos(float64(angle))*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) y0 := float64(originY + int(random.NextIntN(3)) - 2)
y1 := float64(originY + int(random.NextIntN(3)) - 2) y1 := float64(originY + int(random.NextIntN(3)) - 2)

View file

@ -2,6 +2,84 @@ package world
import "regionio/internal/worldgen" 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 { func (r *decorationRegion) placeScheduledOres(seed int64) error {
return r.placeScheduledOresWithOrder(seed, possibleBiomeOrder(), 0) return r.placeScheduledOresWithOrder(seed, possibleBiomeOrder(), 0)
} }

View 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
}

View 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)
}
}
}
}
}

View 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
}

View 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")
}
}

View file

@ -9,6 +9,7 @@ import net.minecraft.util.valueproviders.ClampedNormalInt;
import net.minecraft.util.valueproviders.TrapezoidInt; import net.minecraft.util.valueproviders.TrapezoidInt;
import net.minecraft.world.level.levelgen.LegacyRandomSource; import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.WorldgenRandom; 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.CountPlacement;
import net.minecraft.world.level.levelgen.placement.PlacementModifier; import net.minecraft.world.level.levelgen.placement.PlacementModifier;
import net.minecraft.world.level.levelgen.placement.RandomOffsetPlacement; import net.minecraft.world.level.levelgen.placement.RandomOffsetPlacement;
@ -44,8 +45,14 @@ public final class VanillaPlacementVectors {
WorldgenRandom decoration = new WorldgenRandom(new LegacyRandomSource(0L)); WorldgenRandom decoration = new WorldgenRandom(new LegacyRandomSource(0L));
long decorationSeed = decoration.setDecorationSeed(12345L, 0, 0); long decorationSeed = decoration.setDecorationSeed(12345L, 0, 0);
decoration.setFeatureSeed(decorationSeed, 10, 6); decoration.setFeatureSeed(decorationSeed, 10, 6);
System.out.println("decoration.seed=" + decorationSeed); System.out.println("legacy.decoration.seed=" + decorationSeed);
System.out.println("feature10.stage6=" + randomVector(decoration)); 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) { private static String randomVector(RandomSource random) {