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 {
|
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
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 {
|
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
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 {
|
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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.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) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue