diff --git a/internal/world/block_predicate.go b/internal/world/block_predicate.go index e02d83c..e81fb14 100644 --- a/internal/world/block_predicate.go +++ b/internal/world/block_predicate.go @@ -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": diff --git a/internal/world/disks.go b/internal/world/disks.go new file mode 100644 index 0000000..d7fe3ef --- /dev/null +++ b/internal/world/disks.go @@ -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 +} diff --git a/internal/world/disks_test.go b/internal/world/disks_test.go new file mode 100644 index 0000000..5d1f9a2 --- /dev/null +++ b/internal/world/disks_test.go @@ -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) + } +} diff --git a/internal/world/feature_scheduler.go b/internal/world/feature_scheduler.go index 945cce6..e602a77 100644 --- a/internal/world/feature_scheduler.go +++ b/internal/world/feature_scheduler.go @@ -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 diff --git a/internal/world/geodes.go b/internal/world/geodes.go new file mode 100644 index 0000000..fd38929 --- /dev/null +++ b/internal/world/geodes.go @@ -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}} + } +} diff --git a/internal/world/geodes_test.go b/internal/world/geodes_test.go new file mode 100644 index 0000000..92995e1 --- /dev/null +++ b/internal/world/geodes_test.go @@ -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) + } + } +} diff --git a/internal/world/placed_features.go b/internal/world/placed_features.go index da3862a..1d58e10 100644 --- a/internal/world/placed_features.go +++ b/internal/world/placed_features.go @@ -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) diff --git a/internal/world/region_ores.go b/internal/world/region_ores.go index 7d356e8..2adc808 100644 --- a/internal/world/region_ores.go +++ b/internal/world/region_ores.go @@ -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) } diff --git a/internal/world/underwater_magma.go b/internal/world/underwater_magma.go new file mode 100644 index 0000000..5d23237 --- /dev/null +++ b/internal/world/underwater_magma.go @@ -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 +} diff --git a/internal/world/underwater_magma_test.go b/internal/world/underwater_magma_test.go new file mode 100644 index 0000000..6ab6d84 --- /dev/null +++ b/internal/world/underwater_magma_test.go @@ -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) + } + } + } + } +} diff --git a/internal/world/vegetation_patches.go b/internal/world/vegetation_patches.go new file mode 100644 index 0000000..cd094b6 --- /dev/null +++ b/internal/world/vegetation_patches.go @@ -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 +} diff --git a/internal/world/vegetation_patches_test.go b/internal/world/vegetation_patches_test.go new file mode 100644 index 0000000..6e9109c --- /dev/null +++ b/internal/world/vegetation_patches_test.go @@ -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") + } +} diff --git a/tools/VanillaPlacementVectors.java b/tools/VanillaPlacementVectors.java index 92b08b7..5d05951 100644 --- a/tools/VanillaPlacementVectors.java +++ b/tools/VanillaPlacementVectors.java @@ -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) {