From 9ffd2b19ba27b7bf8cab8b92a94ce18930145842 Mon Sep 17 00:00:00 2001 From: Daniar Mannanov Date: Tue, 11 Aug 2026 13:35:47 +0300 Subject: [PATCH] Place scheduled ores across regions --- internal/world/decoration_region.go | 17 ++++ internal/world/region_ores.go | 139 ++++++++++++++++++++++++++++ internal/world/region_ores_test.go | 61 ++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 internal/world/region_ores.go create mode 100644 internal/world/region_ores_test.go diff --git a/internal/world/decoration_region.go b/internal/world/decoration_region.go index cbb7e16..0af95d9 100644 --- a/internal/world/decoration_region.go +++ b/internal/world/decoration_region.go @@ -101,6 +101,23 @@ func (r *decorationRegion) getBiome(x, y, z int) (uint16, bool) { return chunk.GetBiome(x&15, y, z&15), true } +func (r *decorationRegion) biomeAllowsFeature(set *worldgen.FeatureSet, feature string, stage int, position worldgen.FeaturePosition) bool { + id, ok := r.getBiome(position.X, position.Y, position.Z) + if !ok { + return false + } + biome, ok := set.Biomes[biomeNameByID(id)] + if !ok || stage < 0 || stage >= len(biome.Features) { + return false + } + for _, name := range biome.Features[stage] { + if name == feature { + return true + } + } + return false +} + func (r *decorationRegion) sourceBiomes() []string { seen := make(map[uint16]bool) var names []string diff --git a/internal/world/region_ores.go b/internal/world/region_ores.go new file mode 100644 index 0000000..7f7ef87 --- /dev/null +++ b/internal/world/region_ores.go @@ -0,0 +1,139 @@ +package world + +import ( + "math" + + "regionio/internal/worldgen" +) + +func (r *decorationRegion) placeScheduledOres(seed int64) error { + set, err := worldgen.LoadFeatureSet() + if err != nil { + return err + } + schedule, err := r.scheduledFeatures(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 := set.Placed[scheduled.Name] + configured := set.Configured[placed.Feature] + if configured.Type != "minecraft:ore" { + continue + } + config, err := set.Ore(placed.Feature) + if err != nil { + return err + } + targets, ok := resolveOreTargets(set, config) + 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) + }) + positions, err := set.PlacementPositions(scheduled.Name, random, origin, context) + if err != nil { + return err + } + for _, position := range positions { + placeOreEllipsoidRegion(r, random, position.X, position.Y, position.Z, config.Size, config.DiscardAirExposure, targets) + } + } + return nil +} + +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) + + type sphere struct{ x, y, z, radius float64 } + spheres := make([]sphere, 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] = sphere{ + 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 + } + } + } + } + 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++ { + if y < MinY || y >= MinY+WorldHeight { + 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 { + for _, offset := range [][3]int{{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}} { + if region.getBlock(x+offset[0], y+offset[1], z+offset[2]) == StateAir { + return true + } + } + return false +} diff --git a/internal/world/region_ores_test.go b/internal/world/region_ores_test.go new file mode 100644 index 0000000..6972e17 --- /dev/null +++ b/internal/world/region_ores_test.go @@ -0,0 +1,61 @@ +package world + +import "testing" + +func TestScheduledRegionOresAreDeterministic(t *testing.T) { + makeRegion := func() *decorationRegion { + var chunks []*Chunk + for cx := int32(-1); cx <= 1; cx++ { + for cz := int32(-1); cz <= 1; 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 + } + + a, b := makeRegion(), makeRegion() + if err := a.placeScheduledOres(12345); err != nil { + t.Fatal(err) + } + if err := b.placeScheduledOres(12345); err != nil { + t.Fatal(err) + } + changed := 0 + 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++ { + got := left.GetBlock(x, y, z) + if got != right.GetBlock(x, y, z) { + t.Fatalf("regions differ at chunk (%d,%d) block (%d,%d,%d)", cx, cz, x, y, z) + } + if got != StateStone { + changed++ + } + } + } + } + } + } + if changed == 0 { + t.Fatal("scheduled ore pass changed no blocks") + } +}