diff --git a/internal/world/placed_features.go b/internal/world/placed_features.go index a3e1dc4..da3862a 100644 --- a/internal/world/placed_features.go +++ b/internal/world/placed_features.go @@ -108,12 +108,37 @@ func flattenBlockTag(set *worldgen.FeatureSet, tag string, visiting map[string]b } func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY, originZ, size int, discard float64, targets []resolvedOreTarget) { + spheres := buildOreSpheres(random, originX, originY, originZ, size) + walkOreBlocks(spheres, func(x, y, z int) { + localX := x - int(c.X)*16 + if localX < 0 || localX >= 16 { + return + } + if y < MinY || y >= MinY+WorldHeight { + return + } + localZ := z - int(c.Z)*16 + if localZ < 0 || localZ >= 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 + } + c.SetBlock(localX, y, localZ, target.state) + break + } + }) +} + +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) + math.Sin(float64(angle))*float64(extent) - x1 := float64(originX) - math.Sin(float64(angle))*float64(extent) - z0 := float64(originZ) + math.Cos(float64(angle))*float64(extent) - z1 := float64(originZ) - math.Cos(float64(angle))*float64(extent) + 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) y0 := float64(originY + int(random.NextIntN(3)) - 2) y1 := float64(originY + int(random.NextIntN(3)) - 2) @@ -121,7 +146,8 @@ func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY, for i := 0; i < size; i++ { t := float32(i) / float32(size) randomScale := random.NextDouble() * float64(size) / 16.0 - radius := ((float64(worldgen.MthSin(float64(float32(math.Pi)*t)))+1.0)*randomScale + 1.0) / 2.0 + wave := worldgen.MthSin(float64(float32(math.Pi)*t)) + float32(1.0) + radius := (float64(wave)*randomScale + 1.0) / 2.0 spheres[i] = oreSphere{ x: x0 + (x1-x0)*float64(t), y: y0 + (y1-y0)*float64(t), @@ -149,35 +175,35 @@ func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY, } } } - walkOreBlocks(spheres, func(x, y, z int) { - localX := x - int(c.X)*16 - if localX < 0 || localX >= 16 { - return - } - if y < MinY || y >= MinY+WorldHeight { - return - } - localZ := z - int(c.Z)*16 - if localZ < 0 || localZ >= 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 - } - c.SetBlock(localX, y, localZ, target.state) - break - } - }) + return spheres } func walkOreBlocks(spheres []oreSphere, visit func(x, y, z int)) { - minX, maxX, minY, maxY, minZ, maxZ := oreBounds(spheres) - for x := minX; x <= maxX; x++ { - for y := minY; y <= maxY; y++ { - for z := minZ; z <= maxZ; z++ { - if oreContains(spheres, x, y, z) { + visited := make(map[[3]int]bool) + for _, sphere := range spheres { + if sphere.radius < 0 { + continue + } + minX, maxX := int(math.Floor(sphere.x-sphere.radius)), int(math.Floor(sphere.x+sphere.radius)) + minY, maxY := int(math.Floor(sphere.y-sphere.radius)), int(math.Floor(sphere.y+sphere.radius)) + minZ, maxZ := int(math.Floor(sphere.z-sphere.radius)), int(math.Floor(sphere.z+sphere.radius)) + for x := minX; x <= maxX; x++ { + dx := (float64(x) + 0.5 - sphere.x) / sphere.radius + if dx*dx >= 1 { + continue + } + for y := minY; y <= maxY; y++ { + 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 + position := [3]int{x, y, z} + if dx*dx+dy*dy+dz*dz >= 1 || visited[position] { + continue + } + visited[position] = true visit(x, y, z) } } @@ -185,42 +211,6 @@ func walkOreBlocks(spheres []oreSphere, visit func(x, y, z int)) { } } -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 { 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] diff --git a/internal/world/placed_features_test.go b/internal/world/placed_features_test.go index 0c42cb1..c3591f0 100644 --- a/internal/world/placed_features_test.go +++ b/internal/world/placed_features_test.go @@ -47,7 +47,7 @@ func TestPlacedOresAreDeterministic(t *testing.T) { } } -func TestWalkOreBlocksUsesVanillaCoordinateOrder(t *testing.T) { +func TestWalkOreBlocksUsesVanillaSphereOrder(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}, @@ -59,7 +59,8 @@ func TestWalkOreBlocksUsesVanillaCoordinateOrder(t *testing.T) { 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}, + {1, 0, 0}, + {1, -1, 0}, {1, 0, -1}, {1, 0, 1}, {1, 1, 0}, {2, 0, 0}, } if !reflect.DeepEqual(got, want) { diff --git a/internal/world/region_ore_parity_test.go b/internal/world/region_ore_parity_test.go index d1ceb8e..ed80545 100644 --- a/internal/world/region_ore_parity_test.go +++ b/internal/world/region_ore_parity_test.go @@ -4,11 +4,16 @@ import ( "encoding/binary" "io" "os" + "sort" "testing" "regionio/internal/worldgen" ) +type oreDifference struct { + extra, missing int +} + func TestRegionOreReplayParityDiagnostic(t *testing.T) { if os.Getenv("REGIONIO_REGION_ORE_DIAGNOSTIC") != "1" { t.Skip("set REGIONIO_REGION_ORE_DIAGNOSTIC=1 to run region ore replay parity") @@ -62,6 +67,9 @@ func TestRegionOreReplayParityDiagnostic(t *testing.T) { } initCarverReplaceable(carver.ReplaceableBlocks()) var blockTotal, regionExact, regionOreMismatch, centerExact, centerOreMismatch, legacyExact, legacyOreMismatch int + regionByState := make(map[uint16]*oreDifference) + centerByState := make(map[uint16]*oreDifference) + legacyByState := make(map[uint16]*oreDifference) for _, fixture := range fixtures { var chunks []*Chunk for cx := fixture.x - 2; cx <= fixture.x+2; cx++ { @@ -117,16 +125,19 @@ func TestRegionOreReplayParityDiagnostic(t *testing.T) { regionExact++ } else if isOreState(regionGot) || isOreState(want) { regionOreMismatch++ + countOreDifference(regionByState, regionGot, want) } if centerGot == want { centerExact++ } else if isOreState(centerGot) || isOreState(want) { centerOreMismatch++ + countOreDifference(centerByState, centerGot, want) } if legacyGot == want { legacyExact++ } else if isOreState(legacyGot) || isOreState(want) { legacyOreMismatch++ + countOreDifference(legacyByState, legacyGot, want) } } } @@ -135,4 +146,40 @@ func TestRegionOreReplayParityDiagnostic(t *testing.T) { t.Logf("region ore replay block exact %d/%d (%.3f%%), ore mismatches %d", regionExact, blockTotal, percent(regionExact, blockTotal), regionOreMismatch) t.Logf("center-only generic block exact %d/%d (%.3f%%), ore mismatches %d", centerExact, blockTotal, percent(centerExact, blockTotal), centerOreMismatch) t.Logf("legacy ore-only block exact %d/%d (%.3f%%), ore mismatches %d", legacyExact, blockTotal, percent(legacyExact, blockTotal), legacyOreMismatch) + logOreDifferences(t, "region", regionByState) + logOreDifferences(t, "center", centerByState) + logOreDifferences(t, "legacy", legacyByState) +} + +func countOreDifference(counts map[uint16]*oreDifference, got, want uint16) { + if isOreState(got) { + entry := counts[got] + if entry == nil { + entry = &oreDifference{} + counts[got] = entry + } + entry.extra++ + } + if isOreState(want) { + entry := counts[want] + if entry == nil { + entry = &oreDifference{} + counts[want] = entry + } + entry.missing++ + } +} + +func logOreDifferences(t *testing.T, label string, counts map[uint16]*oreDifference) { + t.Helper() + states := make([]int, 0, len(counts)) + for state := range counts { + states = append(states, int(state)) + } + sort.Ints(states) + for _, value := range states { + state := uint16(value) + entry := counts[state] + t.Logf("%s %s: extra=%d missing=%d", label, stateLabel(state), entry.extra, entry.missing) + } } diff --git a/internal/world/region_ores.go b/internal/world/region_ores.go index ca05ea5..4536b29 100644 --- a/internal/world/region_ores.go +++ b/internal/world/region_ores.go @@ -1,10 +1,6 @@ package world -import ( - "math" - - "regionio/internal/worldgen" -) +import "regionio/internal/worldgen" func (r *decorationRegion) placeScheduledOres(seed int64) error { set, err := worldgen.LoadFeatureSet() @@ -47,45 +43,7 @@ func (r *decorationRegion) placeScheduledOres(seed int64) error { } func placeOreEllipsoidRegion(region *decorationRegion, random worldgen.RandomSource, originX, originY, originZ, size int, discard float64, targets []resolvedOreTarget) { - angle := random.NextFloat() * float32(math.Pi) - extent := float32(size) / 8.0 - x0 := float64(originX) + math.Sin(float64(angle))*float64(extent) - x1 := float64(originX) - math.Sin(float64(angle))*float64(extent) - z0 := float64(originZ) + math.Cos(float64(angle))*float64(extent) - z1 := float64(originZ) - math.Cos(float64(angle))*float64(extent) - y0 := float64(originY + int(random.NextIntN(3)) - 2) - y1 := float64(originY + int(random.NextIntN(3)) - 2) - - spheres := make([]oreSphere, size) - for i := 0; i < size; i++ { - t := float32(i) / float32(size) - randomScale := random.NextDouble() * float64(size) / 16.0 - radius := ((float64(worldgen.MthSin(float64(float32(math.Pi)*t)))+1.0)*randomScale + 1.0) / 2.0 - spheres[i] = oreSphere{ - x: x0 + (x1-x0)*float64(t), y: y0 + (y1-y0)*float64(t), - z: z0 + (z1-z0)*float64(t), radius: radius, - } - } - for i := range spheres { - if spheres[i].radius < 0 { - continue - } - for j := i + 1; j < len(spheres); j++ { - if spheres[j].radius < 0 { - continue - } - dx, dy, dz := spheres[i].x-spheres[j].x, spheres[i].y-spheres[j].y, spheres[i].z-spheres[j].z - dr := spheres[i].radius - spheres[j].radius - if dr*dr > dx*dx+dy*dy+dz*dz { - if dr > 0 { - spheres[j].radius = -1 - } else { - spheres[i].radius = -1 - break - } - } - } - } + spheres := buildOreSpheres(random, originX, originY, originZ, size) walkOreBlocks(spheres, func(x, y, z int) { if y < MinY || y >= MinY+WorldHeight { return diff --git a/internal/world/store.go b/internal/world/store.go index bfc89ca..7011bf6 100644 --- a/internal/world/store.go +++ b/internal/world/store.go @@ -33,7 +33,7 @@ const dataVersion26 = 4790 // 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 // looks like it did nothing in exactly the place you are standing. -const generatorVersion = 18 +const generatorVersion = 19 // generatorVersionTag is the NBT key holding generatorVersion. It is namespaced // because it is ours, not part of the vanilla chunk format.