Match vanilla ore block traversal

This commit is contained in:
Daniar Mannanov 2026-08-17 00:45:32 +03:00
parent 400e76382a
commit c68c5c2744
4 changed files with 103 additions and 85 deletions

View file

@ -13,6 +13,8 @@ type resolvedOreTarget struct {
replaceables map[uint16]bool replaceables map[uint16]bool
} }
type oreSphere struct{ x, y, z, radius float64 }
func placeVanillaOres(c *Chunk, seed int64, cx, cz int32, biomes *[16][16]string) { func placeVanillaOres(c *Chunk, seed int64, cx, cz int32, biomes *[16][16]string) {
set, err := worldgen.LoadFeatureSet() set, err := worldgen.LoadFeatureSet()
if err != nil { if err != nil {
@ -115,13 +117,12 @@ func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY,
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)
type sphere struct{ x, y, z, radius float64 } spheres := make([]oreSphere, size)
spheres := make([]sphere, size)
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
t := float32(i) / float32(size) t := float32(i) / float32(size)
randomScale := random.NextDouble() * float64(size) / 16.0 randomScale := random.NextDouble() * float64(size) / 16.0
radius := ((float64(worldgen.MthSin(float64(float32(math.Pi)*t)))+1.0)*randomScale + 1.0) / 2.0 radius := ((float64(worldgen.MthSin(float64(float32(math.Pi)*t)))+1.0)*randomScale + 1.0) / 2.0
spheres[i] = sphere{ spheres[i] = oreSphere{
x: x0 + (x1-x0)*float64(t), x: x0 + (x1-x0)*float64(t),
y: y0 + (y1-y0)*float64(t), y: y0 + (y1-y0)*float64(t),
z: z0 + (z1-z0)*float64(t), z: z0 + (z1-z0)*float64(t),
@ -148,56 +149,78 @@ func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY,
} }
} }
} }
visited := make(map[[3]int]bool) walkOreBlocks(spheres, func(x, y, z int) {
for _, sphere := range spheres { localX := x - int(c.X)*16
if sphere.radius < 0 { if localX < 0 || localX >= 16 {
continue return
} }
minX, maxX := int(math.Floor(sphere.x-sphere.radius)), int(math.Floor(sphere.x+sphere.radius)) if y < MinY || y >= MinY+WorldHeight {
minY, maxY := int(math.Floor(sphere.y-sphere.radius)), int(math.Floor(sphere.y+sphere.radius)) return
minZ, maxZ := int(math.Floor(sphere.z-sphere.radius)), int(math.Floor(sphere.z+sphere.radius)) }
for x := minX; x <= maxX; x++ { localZ := z - int(c.Z)*16
localX := x - int(c.X)*16 if localZ < 0 || localZ >= 16 {
if localX < 0 || localX >= 16 { return
}
current := c.GetBlock(localX, y, localZ)
for _, target := range targets {
if !target.replaceables[current] || discard > 0 && random.NextFloat() < float32(discard) && exposedToAir(c, localX, y, localZ) {
continue continue
} }
dx := (float64(x) + 0.5 - sphere.x) / sphere.radius c.SetBlock(localX, y, localZ, target.state)
if dx*dx >= 1 { break
continue }
} })
for y := minY; y <= maxY; y++ { }
if y < MinY || y >= MinY+WorldHeight {
continue func walkOreBlocks(spheres []oreSphere, visit func(x, y, z int)) {
} minX, maxX, minY, maxY, minZ, maxZ := oreBounds(spheres)
dy := (float64(y) + 0.5 - sphere.y) / sphere.radius for x := minX; x <= maxX; x++ {
if dx*dx+dy*dy >= 1 { for y := minY; y <= maxY; y++ {
continue for z := minZ; z <= maxZ; z++ {
} if oreContains(spheres, x, y, z) {
for z := minZ; z <= maxZ; z++ { visit(x, y, z)
localZ := z - int(c.Z)*16
if localZ < 0 || localZ >= 16 {
continue
}
dz := (float64(z) + 0.5 - sphere.z) / sphere.radius
pos := [3]int{localX, y, localZ}
if dx*dx+dy*dy+dz*dz >= 1 || visited[pos] {
continue
}
visited[pos] = true
current := c.GetBlock(localX, y, localZ)
for _, target := range targets {
if !target.replaceables[current] || discard > 0 && random.NextFloat() < float32(discard) && exposedToAir(c, localX, y, localZ) {
continue
}
c.SetBlock(localX, y, localZ, target.state)
break
}
} }
} }
} }
} }
} }
func oreBounds(spheres []oreSphere) (minX, maxX, minY, maxY, minZ, maxZ int) {
found := false
for _, sphere := range spheres {
if sphere.radius < 0 {
continue
}
loX, hiX := int(math.Floor(sphere.x-sphere.radius)), int(math.Floor(sphere.x+sphere.radius))
loY, hiY := int(math.Floor(sphere.y-sphere.radius)), int(math.Floor(sphere.y+sphere.radius))
loZ, hiZ := int(math.Floor(sphere.z-sphere.radius)), int(math.Floor(sphere.z+sphere.radius))
if !found {
minX, maxX, minY, maxY, minZ, maxZ = loX, hiX, loY, hiY, loZ, hiZ
found = true
continue
}
minX, maxX = min(minX, loX), max(maxX, hiX)
minY, maxY = min(minY, loY), max(maxY, hiY)
minZ, maxZ = min(minZ, loZ), max(maxZ, hiZ)
}
return
}
func oreContains(spheres []oreSphere, x, y, z int) bool {
for _, sphere := range spheres {
if sphere.radius < 0 {
continue
}
dx := (float64(x) + 0.5 - sphere.x) / sphere.radius
dy := (float64(y) + 0.5 - sphere.y) / sphere.radius
dz := (float64(z) + 0.5 - sphere.z) / sphere.radius
if dx*dx+dy*dy+dz*dz < 1 {
return true
}
}
return false
}
func exposedToAir(c *Chunk, x, y, z int) bool { func exposedToAir(c *Chunk, x, y, z int) bool {
for _, offset := range [][3]int{{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}} { for _, offset := range [][3]int{{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}} {
nx, ny, nz := x+offset[0], y+offset[1], z+offset[2] nx, ny, nz := x+offset[0], y+offset[1], z+offset[2]

View file

@ -1,6 +1,9 @@
package world package world
import "testing" import (
"reflect"
"testing"
)
func TestPlacedOresUseStoneAndDeepslateTargets(t *testing.T) { func TestPlacedOresUseStoneAndDeepslateTargets(t *testing.T) {
gen := NewVanillaGenerator(12345) gen := NewVanillaGenerator(12345)
@ -43,3 +46,23 @@ func TestPlacedOresAreDeterministic(t *testing.T) {
} }
} }
} }
func TestWalkOreBlocksUsesVanillaCoordinateOrder(t *testing.T) {
spheres := []oreSphere{
{x: 0.5, y: 0.5, z: 0.5, radius: 1.1},
{x: 1.5, y: 0.5, z: 0.5, radius: 1.1},
}
var got [][3]int
walkOreBlocks(spheres, func(x, y, z int) {
got = append(got, [3]int{x, y, z})
})
want := [][3]int{
{-1, 0, 0},
{0, -1, 0}, {0, 0, -1}, {0, 0, 0}, {0, 0, 1}, {0, 1, 0},
{1, -1, 0}, {1, 0, -1}, {1, 0, 0}, {1, 0, 1}, {1, 1, 0},
{2, 0, 0},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("walkOreBlocks = %v, want %v", got, want)
}
}

View file

@ -56,13 +56,12 @@ func placeOreEllipsoidRegion(region *decorationRegion, random worldgen.RandomSou
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)
type sphere struct{ x, y, z, radius float64 } spheres := make([]oreSphere, size)
spheres := make([]sphere, size)
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
t := float32(i) / float32(size) t := float32(i) / float32(size)
randomScale := random.NextDouble() * float64(size) / 16.0 randomScale := random.NextDouble() * float64(size) / 16.0
radius := ((float64(worldgen.MthSin(float64(float32(math.Pi)*t)))+1.0)*randomScale + 1.0) / 2.0 radius := ((float64(worldgen.MthSin(float64(float32(math.Pi)*t)))+1.0)*randomScale + 1.0) / 2.0
spheres[i] = sphere{ spheres[i] = oreSphere{
x: x0 + (x1-x0)*float64(t), y: y0 + (y1-y0)*float64(t), x: x0 + (x1-x0)*float64(t), y: y0 + (y1-y0)*float64(t),
z: z0 + (z1-z0)*float64(t), radius: radius, z: z0 + (z1-z0)*float64(t), radius: radius,
} }
@ -87,46 +86,19 @@ func placeOreEllipsoidRegion(region *decorationRegion, random worldgen.RandomSou
} }
} }
} }
visited := make(map[[3]int]bool) walkOreBlocks(spheres, func(x, y, z int) {
for _, sphere := range spheres { if y < MinY || y >= MinY+WorldHeight {
if sphere.radius < 0 { return
continue
} }
minX, maxX := int(math.Floor(sphere.x-sphere.radius)), int(math.Floor(sphere.x+sphere.radius)) current := region.getBlock(x, y, z)
minY, maxY := int(math.Floor(sphere.y-sphere.radius)), int(math.Floor(sphere.y+sphere.radius)) for _, target := range targets {
minZ, maxZ := int(math.Floor(sphere.z-sphere.radius)), int(math.Floor(sphere.z+sphere.radius)) if !target.replaceables[current] || discard > 0 && random.NextFloat() < float32(discard) && exposedToAirRegion(region, x, y, z) {
for x := minX; x <= maxX; x++ {
dx := (float64(x) + 0.5 - sphere.x) / sphere.radius
if dx*dx >= 1 {
continue continue
} }
for y := minY; y <= maxY; y++ { region.setBlock(x, y, z, target.state)
if y < MinY || y >= MinY+WorldHeight { break
continue
}
dy := (float64(y) + 0.5 - sphere.y) / sphere.radius
if dx*dx+dy*dy >= 1 {
continue
}
for z := minZ; z <= maxZ; z++ {
dz := (float64(z) + 0.5 - sphere.z) / sphere.radius
pos := [3]int{x, y, z}
if dx*dx+dy*dy+dz*dz >= 1 || visited[pos] {
continue
}
visited[pos] = true
current := region.getBlock(x, y, z)
for _, target := range targets {
if !target.replaceables[current] || discard > 0 && random.NextFloat() < float32(discard) && exposedToAirRegion(region, x, y, z) {
continue
}
region.setBlock(x, y, z, target.state)
break
}
}
}
} }
} })
} }
func exposedToAirRegion(region *decorationRegion, x, y, z int) bool { func exposedToAirRegion(region *decorationRegion, x, y, z int) bool {

View file

@ -33,7 +33,7 @@ const dataVersion26 = 4790
// first time it ran: chunkAt prefers the store over the generator, so the // first time it ran: chunkAt prefers the store over the generator, so the
// already-explored area around spawn keeps its old terrain and every later fix // already-explored area around spawn keeps its old terrain and every later fix
// looks like it did nothing in exactly the place you are standing. // looks like it did nothing in exactly the place you are standing.
const generatorVersion = 17 const generatorVersion = 18
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced // generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
// because it is ours, not part of the vanilla chunk format. // because it is ours, not part of the vanilla chunk format.