194 lines
6.5 KiB
Go
194 lines
6.5 KiB
Go
package world
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"regionio/internal/worldgen"
|
|
)
|
|
|
|
// vanillaTerrainCache stores immutable, undecorated terrain snapshots shared
|
|
// by overlapping region requests. A region generator must still clone these
|
|
// chunks before mutable feature replay, but neighboring cache misses no longer
|
|
// rerun the expensive density/carver stage for the same coordinates.
|
|
type vanillaTerrainCache struct {
|
|
mu sync.Mutex
|
|
chunks map[[2]int32]*Chunk
|
|
loads map[[2]int32]*terrainLoad
|
|
max int
|
|
}
|
|
|
|
type terrainLoad struct {
|
|
done chan struct{}
|
|
chunk *Chunk
|
|
}
|
|
|
|
func newVanillaTerrainCache(max int) *vanillaTerrainCache {
|
|
return &vanillaTerrainCache{
|
|
chunks: make(map[[2]int32]*Chunk),
|
|
loads: make(map[[2]int32]*terrainLoad),
|
|
max: max,
|
|
}
|
|
}
|
|
|
|
func (c *vanillaTerrainCache) get(key [2]int32, build func() *Chunk) *Chunk {
|
|
c.mu.Lock()
|
|
if chunk := c.chunks[key]; chunk != nil {
|
|
c.mu.Unlock()
|
|
return chunk
|
|
}
|
|
if load := c.loads[key]; load != nil {
|
|
c.mu.Unlock()
|
|
<-load.done
|
|
return load.chunk
|
|
}
|
|
load := &terrainLoad{done: make(chan struct{})}
|
|
c.loads[key] = load
|
|
c.mu.Unlock()
|
|
|
|
chunk := build()
|
|
c.mu.Lock()
|
|
if existing := c.chunks[key]; existing != nil {
|
|
load.chunk = existing
|
|
delete(c.loads, key)
|
|
close(load.done)
|
|
c.mu.Unlock()
|
|
return existing
|
|
}
|
|
if len(c.chunks) >= c.max {
|
|
// The cache is an optimization only. Evict one arbitrary old entry when
|
|
// full; correctness never depends on retaining a particular chunk.
|
|
for oldKey := range c.chunks {
|
|
delete(c.chunks, oldKey)
|
|
break
|
|
}
|
|
}
|
|
c.chunks[key] = chunk
|
|
load.chunk = chunk
|
|
delete(c.loads, key)
|
|
close(load.done)
|
|
c.mu.Unlock()
|
|
return chunk
|
|
}
|
|
|
|
func terrainClone(chunk *Chunk) *Chunk {
|
|
clone, _ := chunk.snapshot()
|
|
return clone
|
|
}
|
|
|
|
// NewVanillaRegionGenerator builds a target from a mutable five-by-five base
|
|
// neighborhood. Vanilla feature placement for a center chunk can inspect and
|
|
// write into adjacent chunks; the radius-two base supplies the complete source
|
|
// biome neighborhood needed by the nine source centers around that target.
|
|
//
|
|
// This generator is intentionally separate from NewVanillaGenerator while its
|
|
// full decoration parity is being measured. It uses the vanilla-compatible
|
|
// Xoroshiro feature RNG and region ore replay, then applies the remaining
|
|
// non-ore decoration to the target.
|
|
func NewVanillaRegionGenerator(seed int64) Generator {
|
|
od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
|
|
return vanillaRegionGeneratorFromInputs(seed, od, fluidPicker, veins, carver, newVanillaTerrainCache(256))
|
|
}
|
|
|
|
// NewVanillaRegionBatchGenerator builds one complete 3x3 target batch from a
|
|
// shared 7x7 base terrain neighborhood. Each target receives private clones of
|
|
// its 5x5 mutable decoration region, so cross-chunk feature writes cannot leak
|
|
// into the neighboring target's generation.
|
|
func NewVanillaRegionBatchGenerator(seed int64) BatchGenerator {
|
|
od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
|
|
return vanillaRegionBatchGeneratorFromInputs(seed, od, fluidPicker, veins, carver, newVanillaTerrainCache(256))
|
|
}
|
|
|
|
// NewVanillaRegionGenerators returns the region-faithful single and batch
|
|
// generators sharing one immutable worldgen input set.
|
|
func NewVanillaRegionGenerators(seed int64) (Generator, BatchGenerator) {
|
|
od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
|
|
terrain := newVanillaTerrainCache(256)
|
|
return vanillaRegionGeneratorFromInputs(seed, od, fluidPicker, veins, carver, terrain),
|
|
vanillaRegionBatchGeneratorFromInputs(seed, od, fluidPicker, veins, carver, terrain)
|
|
}
|
|
|
|
func vanillaRegionGeneratorFromInputs(seed int64, od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, terrain *vanillaTerrainCache) Generator {
|
|
return func(targetX, targetZ int32) *Chunk {
|
|
chunks := make([]*Chunk, 0, 25)
|
|
for cx := targetX - 2; cx <= targetX+2; cx++ {
|
|
for cz := targetZ - 2; cz <= targetZ+2; cz++ {
|
|
key := [2]int32{cx, cz}
|
|
base := terrain.get(key, func() *Chunk {
|
|
return generateVanillaWithoutDecoration(od, fluidPicker, veins, carver, seed, cx, cz)
|
|
})
|
|
chunks = append(chunks, terrainClone(base))
|
|
}
|
|
}
|
|
region, err := newDecorationRegion(chunks)
|
|
if err != nil {
|
|
panic("world: creating decoration region: " + err.Error())
|
|
}
|
|
if err := region.replayScheduledOres(seed, targetX, targetZ); err != nil {
|
|
panic("world: replaying region ores: " + err.Error())
|
|
}
|
|
target := region.chunks[[2]int32{targetX, targetZ}]
|
|
decorateGeneratedNonOre(target, od, seed)
|
|
return target
|
|
}
|
|
}
|
|
|
|
func vanillaRegionBatchGeneratorFromInputs(seed int64, od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, terrain *vanillaTerrainCache) BatchGenerator {
|
|
return func(targetX, targetZ int32) (map[[2]int32]*Chunk, error) {
|
|
base := make(map[[2]int32]*Chunk, 49)
|
|
for cx := targetX - 3; cx <= targetX+3; cx++ {
|
|
for cz := targetZ - 3; cz <= targetZ+3; cz++ {
|
|
key := [2]int32{cx, cz}
|
|
base[key] = terrain.get(key, func() *Chunk {
|
|
return generateVanillaWithoutDecoration(od, fluidPicker, veins, carver, seed, cx, cz)
|
|
})
|
|
}
|
|
}
|
|
batch := make(map[[2]int32]*Chunk, 9)
|
|
for cx := targetX - 1; cx <= targetX+1; cx++ {
|
|
for cz := targetZ - 1; cz <= targetZ+1; cz++ {
|
|
chunks := make([]*Chunk, 0, 25)
|
|
for sx := cx - 2; sx <= cx+2; sx++ {
|
|
for sz := cz - 2; sz <= cz+2; sz++ {
|
|
baseChunk := base[[2]int32{sx, sz}]
|
|
clone, _ := baseChunk.snapshot()
|
|
chunks = append(chunks, clone)
|
|
}
|
|
}
|
|
region, err := newDecorationRegion(chunks)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := region.replayScheduledOres(seed, cx, cz); err != nil {
|
|
return nil, err
|
|
}
|
|
target := region.chunks[[2]int32{cx, cz}]
|
|
decorateGeneratedNonOre(target, od, seed)
|
|
batch[[2]int32{cx, cz}] = target
|
|
}
|
|
}
|
|
return batch, nil
|
|
}
|
|
}
|
|
|
|
func decorateGeneratedNonOre(c *Chunk, od *worldgen.OverworldDensity, seed int64) {
|
|
var surfTop [16][16]int
|
|
var grass [16][16]bool
|
|
var biomeName [16][16]string
|
|
baseX, baseZ := int(c.X)*16, int(c.Z)*16
|
|
for x := 0; x < 16; x++ {
|
|
for z := 0; z < 16; z++ {
|
|
surfTop[x][z], grass[x][z] = classifyColumnAtSurface(c, x, z)
|
|
biomeName[x][z] = BiomeNameAt(od, baseX+x, baseZ+z)
|
|
}
|
|
}
|
|
r := newChunkRand(c.X, c.Z, seed)
|
|
decorateNonOre(c, od, c.X, c.Z, seed, &surfTop, &grass, &biomeName, &r)
|
|
}
|
|
|
|
func classifyColumnAtSurface(c *Chunk, x, z int) (top int, grass bool) {
|
|
var column [WorldHeight]uint16
|
|
for i := 0; i < WorldHeight; i++ {
|
|
column[i] = c.GetBlock(x, MinY+i, z)
|
|
}
|
|
return classifyColumn(&column)
|
|
}
|