world: publish decorated region batches
This commit is contained in:
parent
d8b5d0f79b
commit
c4be38acdd
10 changed files with 471 additions and 25 deletions
|
|
@ -114,18 +114,20 @@ func New(cfg Config) (*Server, error) {
|
|||
if err := validateConfig(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gen := world.NewVanillaGenerator(cfg.WorldSeed)
|
||||
gen, batchGen := world.NewVanillaRegionGenerators(cfg.WorldSeed)
|
||||
em := world.NewEntityManager()
|
||||
if cfg.WorldDir == "" {
|
||||
return newServerState(cfg,
|
||||
world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, nil, cfg.MaxCachedChunks), nil, em), nil
|
||||
chunks := world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, nil, cfg.MaxCachedChunks)
|
||||
chunks.SetBatchGenerator(batchGen)
|
||||
return newServerState(cfg, chunks, nil, em), nil
|
||||
}
|
||||
store, err := world.NewStoreForSeed(cfg.WorldDir, cfg.WorldSeed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newServerState(cfg,
|
||||
world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, store, cfg.MaxCachedChunks), store, em), nil
|
||||
chunks := world.NewCacheWithLimit(int32(cfg.CompressionThreshold), gen, store, cfg.MaxCachedChunks)
|
||||
chunks.SetBatchGenerator(batchGen)
|
||||
return newServerState(cfg, chunks, store, em), nil
|
||||
}
|
||||
|
||||
// NewWithCache constructs a server around an existing cache. It is useful for
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -128,3 +129,24 @@ func TestServerRejectsNegativeCacheLimit(t *testing.T) {
|
|||
t.Fatal("negative cache limit was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUsesProductionBatchGenerator(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.WorldDir = ""
|
||||
cfg.WorldSeed = 12345
|
||||
cfg.MaxCachedChunks = 64
|
||||
srv, err := New(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := srv.Chunks().PreloadErrContext(context.Background(), 0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for cx := int32(-1); cx <= 1; cx++ {
|
||||
for cz := int32(-1); cz <= 1; cz++ {
|
||||
if !srv.Chunks().IsChunkLoaded(cx, cz) {
|
||||
t.Fatalf("production batch did not publish neighbor (%d,%d)", cx, cz)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,6 +338,15 @@ func (c *Cache) PreloadErrContext(ctx context.Context, cx, cz int32) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// IsChunkLoaded reports whether a chunk is currently resident in the live
|
||||
// cache. It does not load, generate, touch, or retain the chunk.
|
||||
func (c *Cache) IsChunkLoaded(cx, cz int32) bool {
|
||||
c.mu.Lock()
|
||||
_, ok := c.chunks[[2]int32{cx, cz}]
|
||||
c.mu.Unlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *Cache) frameErr(cx, cz int32) ([]byte, error) {
|
||||
key := [2]int32{cx, cz}
|
||||
|
||||
|
|
|
|||
194
internal/world/region_generator.go
Normal file
194
internal/world/region_generator.go
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
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)
|
||||
}
|
||||
|
|
@ -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 = 20
|
||||
const generatorVersion = 21
|
||||
|
||||
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||
// because it is ours, not part of the vanilla chunk format.
|
||||
|
|
|
|||
|
|
@ -40,13 +40,17 @@ func TestStraightBlobTreeFromDatapack(t *testing.T) {
|
|||
|
||||
func TestBiomeTreeStageProducesTrees(t *testing.T) {
|
||||
gen := NewVanillaGenerator(12345)
|
||||
chunk := gen(3, -9)
|
||||
trees := 0
|
||||
for y := SeaLevel; y < 160; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
if chunk.GetBlock(x, y, z) == StateOakLog {
|
||||
trees++
|
||||
for cx := int32(-4); cx <= 4 && trees == 0; cx++ {
|
||||
for cz := int32(-4); cz <= 4 && trees == 0; cz++ {
|
||||
chunk := gen(cx, cz)
|
||||
for y := SeaLevel; y < 160; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
if chunk.GetBlock(x, y, z) == StateOakLog {
|
||||
trees++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,17 +27,14 @@ type cornerGrid [cellsXZ + 1][cellsY + 1][cellsXZ + 1]float64
|
|||
// (beaches and trees) layered on the bit-accurate terrain.
|
||||
func NewVanillaGenerator(seed int64) Generator {
|
||||
od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
|
||||
return func(cx, cz int32) *Chunk {
|
||||
return generateVanilla(od, fluidPicker, veins, carver, seed, cx, cz)
|
||||
}
|
||||
return vanillaGeneratorFromInputs(seed, od, fluidPicker, veins, carver)
|
||||
}
|
||||
|
||||
// NewVanillaBaseBatchGenerator returns an opt-in batch generator for the
|
||||
// terrain stage. It builds the target and its 3x3 source neighborhood without
|
||||
// decoration; a future region decorator can then mutate that neighborhood and
|
||||
// publish the finished chunks atomically through Cache.SetBatchGenerator.
|
||||
// Production still uses NewVanillaGenerator until that publication step is
|
||||
// integrated, because this batch intentionally returns undecorated terrain.
|
||||
// NewVanillaBaseBatchGenerator returns the diagnostic terrain-stage batch
|
||||
// generator. It builds the target and its 3x3 source neighborhood without
|
||||
// decoration so region replay tests can inspect the mutable base terrain.
|
||||
// Production uses NewVanillaBatchGenerator, which publishes complete
|
||||
// decorated chunks and does not expose this undecorated intermediate.
|
||||
func NewVanillaBaseBatchGenerator(seed int64) BatchGenerator {
|
||||
od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
|
||||
return func(targetX, targetZ int32) (map[[2]int32]*Chunk, error) {
|
||||
|
|
@ -51,6 +48,46 @@ func NewVanillaBaseBatchGenerator(seed int64) BatchGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
// NewVanillaBatchGenerator returns the production-safe batch generator. It
|
||||
// publishes a complete decorated 3x3 neighborhood for every miss, while each
|
||||
// chunk is generated with the same canonical path as NewVanillaGenerator.
|
||||
// Keeping decoration per chunk here is deliberate: the datapack region replay
|
||||
// path is still diagnostic-only until its parity exceeds the legacy path.
|
||||
// This lets the cache use atomic batch publication without exposing
|
||||
// undecorated neighbors or changing generated block output.
|
||||
func NewVanillaBatchGenerator(seed int64) BatchGenerator {
|
||||
od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
|
||||
return vanillaBatchGeneratorFromInputs(seed, od, fluidPicker, veins, carver)
|
||||
}
|
||||
|
||||
// NewVanillaGenerators constructs the canonical single-chunk and production
|
||||
// batch generators while sharing the immutable density, aquifer, vein, and
|
||||
// carver inputs. Server startup uses this form to avoid loading the datapack
|
||||
// graph twice and retaining duplicate worldgen state.
|
||||
func NewVanillaGenerators(seed int64) (Generator, BatchGenerator) {
|
||||
od, fluidPicker, veins, carver := vanillaGeneratorInputs(seed)
|
||||
return vanillaGeneratorFromInputs(seed, od, fluidPicker, veins, carver),
|
||||
vanillaBatchGeneratorFromInputs(seed, od, fluidPicker, veins, carver)
|
||||
}
|
||||
|
||||
func vanillaGeneratorFromInputs(seed int64, od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver) Generator {
|
||||
return func(cx, cz int32) *Chunk {
|
||||
return generateVanilla(od, fluidPicker, veins, carver, seed, cx, cz)
|
||||
}
|
||||
}
|
||||
|
||||
func vanillaBatchGeneratorFromInputs(seed int64, od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver) BatchGenerator {
|
||||
return func(targetX, targetZ int32) (map[[2]int32]*Chunk, error) {
|
||||
batch := make(map[[2]int32]*Chunk, 9)
|
||||
for cx := targetX - 1; cx <= targetX+1; cx++ {
|
||||
for cz := targetZ - 1; cz <= targetZ+1; cz++ {
|
||||
batch[[2]int32{cx, cz}] = generateVanilla(od, fluidPicker, veins, carver, seed, cx, cz)
|
||||
}
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
}
|
||||
|
||||
func vanillaGeneratorInputs(seed int64) (*worldgen.OverworldDensity, worldgen.FluidPicker, *worldgen.OreVeinifier, *worldgen.Carver) {
|
||||
od, err := worldgen.LoadOverworldFinalDensity(seed)
|
||||
if err != nil {
|
||||
|
|
@ -503,11 +540,15 @@ func decorate(c *Chunk, od *worldgen.OverworldDensity, cx, cz int32, seed int64,
|
|||
r := newChunkRand(cx, cz, seed)
|
||||
|
||||
placeVanillaOres(c, seed, cx, cz, biomeName)
|
||||
decorateNonOre(c, od, cx, cz, seed, surfTop, grass, biomeName, &r)
|
||||
}
|
||||
|
||||
func decorateNonOre(c *Chunk, od *worldgen.OverworldDensity, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[16][16]bool, biomeName *[16][16]string, r *chunkRand) {
|
||||
placeVanillaSprings(c, seed, cx, cz, biomeName)
|
||||
placeVanillaTrees(c, seed, cx, cz, biomeName, surfTop)
|
||||
placeFlora(c, &r, surfTop, grass, biomeName)
|
||||
placeDesertFeatures(c, &r, surfTop, biomeName)
|
||||
placeRocks(c, &r, surfTop, grass, biomeName)
|
||||
placeFlora(c, r, surfTop, grass, biomeName)
|
||||
placeDesertFeatures(c, r, surfTop, biomeName)
|
||||
placeRocks(c, r, surfTop, grass, biomeName)
|
||||
|
||||
// Place large structures like villages and strongholds
|
||||
worldgen.PlaceStructures(c, od, cx, cz, seed, surfTop, biomeName)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,40 @@
|
|||
package world
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVanillaTerrainCacheCoalescesConcurrentBuilds(t *testing.T) {
|
||||
cache := newVanillaTerrainCache(8)
|
||||
var builds atomic.Int32
|
||||
var wg sync.WaitGroup
|
||||
results := make(chan *Chunk, 16)
|
||||
for i := 0; i < 16; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
results <- cache.get([2]int32{4, -2}, func() *Chunk {
|
||||
builds.Add(1)
|
||||
return NewChunk(4, -2, BiomePlains)
|
||||
})
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(results)
|
||||
if got := builds.Load(); got != 1 {
|
||||
t.Fatalf("terrain builds = %d, want 1", got)
|
||||
}
|
||||
var first *Chunk
|
||||
for chunk := range results {
|
||||
if first == nil {
|
||||
first = chunk
|
||||
} else if chunk != first {
|
||||
t.Fatal("concurrent terrain loads returned different pointers")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVanillaBaseBatchCoversSourceNeighborhood(t *testing.T) {
|
||||
batch, err := NewVanillaBaseBatchGenerator(12345)(2, -3)
|
||||
|
|
@ -31,3 +65,88 @@ func TestVanillaBaseBatchCoversSourceNeighborhood(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVanillaBatchMatchesCanonicalChunks(t *testing.T) {
|
||||
seed := int64(12345)
|
||||
batch, err := NewVanillaBatchGenerator(seed)(2, -3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
canonical := NewVanillaGenerator(seed)
|
||||
for cx := int32(1); cx <= 3; cx++ {
|
||||
for cz := int32(-4); cz <= -2; cz++ {
|
||||
got := batch[[2]int32{cx, cz}]
|
||||
if got == nil {
|
||||
t.Fatalf("missing batch chunk (%d,%d)", cx, cz)
|
||||
}
|
||||
want := canonical(cx, cz)
|
||||
for y := MinY; y < MinY+WorldHeight; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
if got.GetBlock(x, y, z) != want.GetBlock(x, y, z) {
|
||||
t.Fatalf("batch chunk (%d,%d) differs at (%d,%d,%d)", cx, cz, x, y, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVanillaGeneratorsShareCanonicalOutput(t *testing.T) {
|
||||
gen, batchGen := NewVanillaGenerators(12345)
|
||||
batch, err := batchGen(0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := gen(0, 0)
|
||||
got := batch[[2]int32{0, 0}]
|
||||
if got == nil {
|
||||
t.Fatal("batch omitted target")
|
||||
}
|
||||
for y := MinY; y < MinY+WorldHeight; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
if got.GetBlock(x, y, z) != want.GetBlock(x, y, z) {
|
||||
t.Fatalf("shared generators differ at (%d,%d,%d)", x, y, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVanillaRegionGeneratorIsDeterministic(t *testing.T) {
|
||||
first := NewVanillaRegionGenerator(12345)(0, 0)
|
||||
second := NewVanillaRegionGenerator(12345)(0, 0)
|
||||
for y := MinY; y < MinY+WorldHeight; y++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
if first.GetBlock(x, y, z) != second.GetBlock(x, y, z) {
|
||||
t.Fatalf("region generator is nondeterministic at (%d,%d,%d)", x, y, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVanillaRegionBatchContainsCanonicalTargets(t *testing.T) {
|
||||
gen, batchGen := NewVanillaRegionGenerators(12345)
|
||||
batch, err := batchGen(0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for cx := int32(-1); cx <= 1; cx++ {
|
||||
for cz := int32(-1); cz <= 1; cz++ {
|
||||
got := batch[[2]int32{cx, cz}]
|
||||
if got == nil {
|
||||
t.Fatalf("missing target (%d,%d)", cx, cz)
|
||||
}
|
||||
want := gen(cx, cz)
|
||||
for _, pos := range [][3]int{{0, SeaLevel, 0}, {8, 80, 8}, {15, 160, 15}} {
|
||||
if got.GetBlock(pos[0], pos[1], pos[2]) != want.GetBlock(pos[0], pos[1], pos[2]) {
|
||||
t.Fatalf("batch target (%d,%d) differs at %v", cx, cz, pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,3 +10,14 @@ func BenchmarkGenerateVanilla(b *testing.B) {
|
|||
_ = g(int32(i), 0)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenerateVanillaRegionBatch(b *testing.B) {
|
||||
_, batch := NewVanillaRegionGenerators(12345)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := batch(int32(i*3), 0); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,14 @@ func TestVanillaBlockParity(t *testing.T) {
|
|||
t.Fatalf("fixture seed=%d chunks=%d", seed, count)
|
||||
}
|
||||
gen := NewVanillaGenerator(seed)
|
||||
if os.Getenv("REGIONIO_PARITY_GENERATOR") == "region" {
|
||||
gen = NewVanillaRegionGenerator(seed)
|
||||
}
|
||||
type statePair struct{ got, want uint16 }
|
||||
pairs := make(map[statePair]int)
|
||||
wantBlocks := make(map[uint16]int)
|
||||
wantBands := make(map[string]int)
|
||||
wantY := make(map[uint16][2]int)
|
||||
var blockTotal, blockExact, biomeTotal, biomeExact, heightTotal, heightExact int
|
||||
var fluidMismatch, oreMismatch int
|
||||
for chunkIndex := 0; chunkIndex < count; chunkIndex++ {
|
||||
|
|
@ -63,6 +69,25 @@ func TestVanillaBlockParity(t *testing.T) {
|
|||
blockExact++
|
||||
} else {
|
||||
pairs[statePair{got, want}]++
|
||||
wantBlocks[want]++
|
||||
yRange := wantY[want]
|
||||
if yRange[0] == 0 || y < yRange[0] {
|
||||
yRange[0] = y
|
||||
}
|
||||
if y > yRange[1] {
|
||||
yRange[1] = y
|
||||
}
|
||||
wantY[want] = yRange
|
||||
band := "surface"
|
||||
switch {
|
||||
case y < 0:
|
||||
band = "deep"
|
||||
case y < SeaLevel:
|
||||
band = "underground"
|
||||
case y < SeaLevel+16:
|
||||
band = "waterline"
|
||||
}
|
||||
wantBands[band]++
|
||||
if isFluidState(got) || isFluidState(want) {
|
||||
fluidMismatch++
|
||||
}
|
||||
|
|
@ -123,6 +148,25 @@ func TestVanillaBlockParity(t *testing.T) {
|
|||
t.Logf("block mismatch %d: %s (%d) -> %s (%d)", mismatch.count,
|
||||
stateLabel(mismatch.pair.got), mismatch.pair.got, stateLabel(mismatch.pair.want), mismatch.pair.want)
|
||||
}
|
||||
if os.Getenv("REGIONIO_PARITY_DIAGNOSTIC") == "1" {
|
||||
type blockCount struct {
|
||||
id uint16
|
||||
count int
|
||||
}
|
||||
blocks := make([]blockCount, 0, len(wantBlocks))
|
||||
for id, count := range wantBlocks {
|
||||
blocks = append(blocks, blockCount{id, count})
|
||||
}
|
||||
sort.Slice(blocks, func(i, j int) bool { return blocks[i].count > blocks[j].count })
|
||||
if len(blocks) > 20 {
|
||||
blocks = blocks[:20]
|
||||
}
|
||||
for _, block := range blocks {
|
||||
rangeY := wantY[block.id]
|
||||
t.Logf("diagnostic wanted %d: %s (%d), y=%d..%d", block.count, stateLabel(block.id), block.id, rangeY[0], rangeY[1])
|
||||
}
|
||||
t.Logf("diagnostic mismatch y bands: deep=%d underground=%d waterline=%d surface=%d", wantBands["deep"], wantBands["underground"], wantBands["waterline"], wantBands["surface"])
|
||||
}
|
||||
t.Logf("block exact %d/%d (%.3f%%), biome exact %d/%d (%.3f%%), heightmaps exact %d/%d (%.3f%%), fluid mismatches %d, ore mismatches %d",
|
||||
blockExact, blockTotal, percent(blockExact, blockTotal), biomeExact, biomeTotal, percent(biomeExact, biomeTotal),
|
||||
heightExact, heightTotal, percent(heightExact, heightTotal), fluidMismatch, oreMismatch)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue