Load vanilla feature stages and place ores
This commit is contained in:
parent
28c11e2fc2
commit
dd2ea56850
10 changed files with 779 additions and 6 deletions
|
|
@ -1,6 +1,10 @@
|
|||
package world
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"regionio/internal/worldgen"
|
||||
)
|
||||
|
||||
// Ore-vein block states, from OreVeinifier.VeinType. Copper's ore is the plain
|
||||
// stone variant and iron's is the deepslate one; neither switches with depth.
|
||||
|
|
@ -21,7 +25,18 @@ const (
|
|||
// the sign of the veininess noise but its window comes from the type, so a
|
||||
// single block outside a window means the guard is wrong.
|
||||
func TestOreVeins(t *testing.T) {
|
||||
gen := NewVanillaGenerator(12345)
|
||||
const seed = 12345
|
||||
od, err := worldgen.LoadOverworldFinalDensity(seed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
picker := worldgen.OverworldFluidPicker(od.SeaLevel)
|
||||
veins := worldgen.NewOreVeinifier(od)
|
||||
carver, err := worldgen.NewCarver(od, seed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
initCarverReplaceable(carver.ReplaceableBlocks())
|
||||
counts := map[uint16]int{}
|
||||
lowest := map[uint16]int{}
|
||||
highest := map[uint16]int{}
|
||||
|
|
@ -33,7 +48,10 @@ func TestOreVeins(t *testing.T) {
|
|||
}
|
||||
for cx := int32(-60); cx <= 60; cx += 20 {
|
||||
for cz := int32(-60); cz <= 60; cz += 20 {
|
||||
ch := gen(cx, cz)
|
||||
// Call the terrain/material pass directly and skip decorate. Ordinary
|
||||
// placed ores share several states with mega-veins and cannot be
|
||||
// distinguished by block ID after decoration.
|
||||
ch := generateVanillaWithoutDecoration(od, picker, veins, carver, seed, cx, cz)
|
||||
for wy := MinY; wy < 60; wy++ {
|
||||
for lx := 0; lx < 16; lx++ {
|
||||
for lz := 0; lz < 16; lz++ {
|
||||
|
|
|
|||
190
internal/world/placed_features.go
Normal file
190
internal/world/placed_features.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"regionio/internal/worldgen"
|
||||
)
|
||||
|
||||
const undergroundOresStage = 6
|
||||
|
||||
type resolvedOreTarget struct {
|
||||
state uint16
|
||||
replaceables map[uint16]bool
|
||||
}
|
||||
|
||||
func placeVanillaOres(c *Chunk, seed int64, cx, cz int32, biomes *[16][16]string) {
|
||||
set, err := worldgen.LoadFeatureSet()
|
||||
if err != nil {
|
||||
panic("world: loading feature datapack: " + err.Error())
|
||||
}
|
||||
random, decorationSeed := worldgen.DecorationRandom(seed, int(cx), int(cz))
|
||||
seen := make(map[string]bool)
|
||||
for bx := 0; bx < 16; bx += 4 {
|
||||
for bz := 0; bz < 16; bz += 4 {
|
||||
biome := set.Biomes[biomes[bx][bz]]
|
||||
if len(biome.Features) <= undergroundOresStage {
|
||||
continue
|
||||
}
|
||||
for featureIndex, name := range biome.Features[undergroundOresStage] {
|
||||
if seen[name] {
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
placed := set.Placed[name]
|
||||
configured := set.Configured[placed.Feature]
|
||||
if configured.Type != "minecraft:ore" {
|
||||
continue
|
||||
}
|
||||
config, err := set.Ore(placed.Feature)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
plan, err := set.Placement(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
targets, ok := resolveOreTargets(set, config)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
random.SetFeatureSeed(decorationSeed, featureIndex, undergroundOresStage)
|
||||
if plan.RarityChance > 0 && random.NextIntN(int32(plan.RarityChance)) != 0 {
|
||||
continue
|
||||
}
|
||||
attempts := plan.Count.Sample(random)
|
||||
for attempt := 0; attempt < attempts; attempt++ {
|
||||
x := int(random.NextIntN(16))
|
||||
z := int(random.NextIntN(16))
|
||||
y := plan.SampleY(random, MinY, WorldHeight)
|
||||
placeOreEllipsoid(c, random, x, y, z, config.Size, config.DiscardAirExposure, targets)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveOreTargets(set *worldgen.FeatureSet, config worldgen.OreFeatureConfig) ([]resolvedOreTarget, bool) {
|
||||
targets := make([]resolvedOreTarget, 0, len(config.Targets))
|
||||
for _, target := range config.Targets {
|
||||
state, ok := nameToStateID(target.State.Name, nil)
|
||||
if !ok || target.Target.PredicateType != "minecraft:tag_match" {
|
||||
return nil, false
|
||||
}
|
||||
replaceables := make(map[uint16]bool)
|
||||
for _, name := range set.BlockTags[target.Target.Tag] {
|
||||
id, ok := nameToStateID(name, nil)
|
||||
if ok {
|
||||
replaceables[id] = true
|
||||
}
|
||||
}
|
||||
if len(replaceables) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
targets = append(targets, resolvedOreTarget{state: state, replaceables: replaceables})
|
||||
}
|
||||
return targets, true
|
||||
}
|
||||
|
||||
func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY, originZ, size int, discard float64, targets []resolvedOreTarget) {
|
||||
angle := float64(random.NextFloat()) * math.Pi
|
||||
extent := float64(size) / 8.0
|
||||
x0 := float64(originX+8) + math.Sin(angle)*extent
|
||||
x1 := float64(originX+8) - math.Sin(angle)*extent
|
||||
z0 := float64(originZ+8) + math.Cos(angle)*extent
|
||||
z1 := float64(originZ+8) - math.Cos(angle)*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 := float64(i) / float64(size)
|
||||
radius := (math.Sin(math.Pi*t) + 1.0) * (float64(random.NextDouble())*float64(size)/16.0 + 1.0) / 2.0
|
||||
spheres[i] = sphere{
|
||||
x: x0 + (x1-x0)*t,
|
||||
y: y0 + (y1-y0)*t,
|
||||
z: z0 + (z1-z0)*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++ {
|
||||
if x < 0 || x >= 16 {
|
||||
continue
|
||||
}
|
||||
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++ {
|
||||
if z < 0 || z >= 16 {
|
||||
continue
|
||||
}
|
||||
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 := c.GetBlock(x, y, z)
|
||||
for _, target := range targets {
|
||||
if !target.replaceables[current] || discard > 0 && random.NextFloat() < float32(discard) && exposedToAir(c, x, y, z) {
|
||||
continue
|
||||
}
|
||||
c.SetBlock(x, y, z, target.state)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
if nx < 0 || nx >= 16 || nz < 0 || nz >= 16 {
|
||||
continue
|
||||
}
|
||||
if c.GetBlock(nx, ny, nz) == StateAir {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
45
internal/world/placed_features_test.go
Normal file
45
internal/world/placed_features_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package world
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPlacedOresUseStoneAndDeepslateTargets(t *testing.T) {
|
||||
gen := NewVanillaGenerator(12345)
|
||||
want := map[uint16]string{
|
||||
131: "iron ore",
|
||||
132: "deepslate iron ore",
|
||||
5307: "diamond ore",
|
||||
5308: "deepslate diamond ore",
|
||||
}
|
||||
counts := make(map[uint16]int)
|
||||
for cx := int32(-8); cx <= 8; cx += 4 {
|
||||
for cz := int32(-8); cz <= 8; cz += 4 {
|
||||
chunk := gen(cx, cz)
|
||||
for y := MinY; y < 128; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
counts[chunk.GetBlock(x, y, z)]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for state, name := range want {
|
||||
if counts[state] == 0 {
|
||||
t.Errorf("no %s generated by placed ore features", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlacedOresAreDeterministic(t *testing.T) {
|
||||
gen := NewVanillaGenerator(4242)
|
||||
a, b := gen(3, -2), gen(3, -2)
|
||||
for y := MinY; y < MinY+WorldHeight; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
if got, want := a.GetBlock(x, y, z), b.GetBlock(x, y, z); got != want {
|
||||
t.Fatalf("block (%d,%d,%d): first %d second %d", x, y, z, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = 12
|
||||
const generatorVersion = 13
|
||||
|
||||
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||
// because it is ours, not part of the vanilla chunk format.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,14 @@ func NewVanillaGenerator(seed int64) Generator {
|
|||
}
|
||||
|
||||
func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32) *Chunk {
|
||||
return generateVanillaDecorated(od, fluidPicker, veins, carver, seed, cx, cz, true)
|
||||
}
|
||||
|
||||
func generateVanillaWithoutDecoration(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32) *Chunk {
|
||||
return generateVanillaDecorated(od, fluidPicker, veins, carver, seed, cx, cz, false)
|
||||
}
|
||||
|
||||
func generateVanillaDecorated(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32, withDecoration bool) *Chunk {
|
||||
c := NewChunk(cx, cz, BiomePlains) // per-cell biomes override below
|
||||
baseX, baseZ := int(cx)*16, int(cz)*16
|
||||
|
||||
|
|
@ -171,7 +179,9 @@ func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPi
|
|||
}
|
||||
}
|
||||
fillBiomes3D(c, od, s2D, baseX, baseZ)
|
||||
decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName)
|
||||
if withDecoration {
|
||||
decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
|
|
@ -468,7 +478,7 @@ func bedrockAt(rng *chunkRand, d int) bool {
|
|||
func decorate(c *Chunk, od *worldgen.OverworldDensity, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[16][16]bool, biomeName *[16][16]string) {
|
||||
r := newChunkRand(cx, cz, seed)
|
||||
|
||||
placeOres(c, &r)
|
||||
placeVanillaOres(c, seed, cx, cz, biomeName)
|
||||
placeFlora(c, &r, surfTop, grass, biomeName)
|
||||
placeDesertFeatures(c, &r, surfTop, biomeName)
|
||||
placeRocks(c, &r, surfTop, grass, biomeName)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue