Resolve block names to the default state, not the first one

blocks.json lists a block's states in getPossibleStates() order -- the property
cartesian product -- and separately marks which one is the default. The parser
declared only id and properties, so the default flag was dropped on the floor
and nameToStateID returned states[0]. Those differ for 642 of the 1168 blocks.

What that produced, measured rather than guessed: every redstone vein in the
world was lit=true and glowing, every sunflower was placed as its own top half
with nothing under it, and oak stairs came out upside down and waterlogged. It
also quietly disagreed with the StateGrass, StateOakLog and StateOakLeaf
constants next door in encode.go, which are the real defaults.

Now it starts from the default state and applies the properties it recognises,
keeping the default's value for an unknown key or an illegal value -- which is
what vanilla does when it reads a palette entry. That matters on the disk path:
a chunk written with a property we no longer know used to decode to a random
corner state instead of something sane.

The signature grows an ok result, because air was doing double duty as both a
real block and "no such name".

Separately, blockPaletteEntry filled the Properties compound by ranging a Go
map. nbt.Compound preserves insertion order precisely so encoding is
deterministic, so saving one chunk twice produced different region-file bytes
for every multi-property block. Keys are sorted now.
This commit is contained in:
Master290 2026-07-27 03:35:39 +03:00
parent 9e91425c5d
commit 113a59e365
14 changed files with 215 additions and 62 deletions

View file

@ -49,8 +49,8 @@ func TestCaveBiomesPresent(t *testing.T) {
tbl := loadBiomeTable() tbl := loadBiomeTable()
for _, c := range []struct { for _, c := range []struct {
name string name string
point worldgen.TargetPoint point worldgen.TargetPoint
}{ }{
{"minecraft:lush_caves", lush}, {"minecraft:lush_caves", lush},
{"minecraft:dripstone_caves", drip}, {"minecraft:dripstone_caves", drip},

View file

@ -38,8 +38,8 @@ var oreSpecs = []oreSpec{
// are untouched. // are untouched.
func placeOres(c *Chunk, r *chunkRand) { func placeOres(c *Chunk, r *chunkRand) {
for _, spec := range oreSpecs { for _, spec := range oreSpecs {
ore := nameToStateID(spec.name, nil) ore, ok := nameToStateID(spec.name, nil)
if ore == StateAir { if !ok {
continue // unknown block name; skip defensively continue // unknown block name; skip defensively
} }
for a := 0; a < spec.attempts; a++ { for a := 0; a < spec.attempts; a++ {
@ -88,12 +88,12 @@ func placeOreBlob(c *Chunk, ore uint16, n int, lx, y, lz int, r *chunkRand) {
// biomeFlowers maps a biome name to the flower blocks that can spawn on its // biomeFlowers maps a biome name to the flower blocks that can spawn on its
// grassy surface. Empty/absent = no flowers. Names resolve to IDs at runtime. // grassy surface. Empty/absent = no flowers. Names resolve to IDs at runtime.
var biomeFlowers = map[string][]string{ var biomeFlowers = map[string][]string{
"minecraft:plains": {"minecraft:dandelion", "minecraft:poppy", "minecraft:azure_bluet", "minecraft:cornflower", "minecraft:oxeye_daisy"}, "minecraft:plains": {"minecraft:dandelion", "minecraft:poppy", "minecraft:azure_bluet", "minecraft:cornflower", "minecraft:oxeye_daisy"},
"minecraft:sunflower_plains": {"minecraft:dandelion", "minecraft:poppy", "minecraft:sunflower"}, "minecraft:sunflower_plains": {"minecraft:dandelion", "minecraft:poppy", "minecraft:sunflower"},
"minecraft:forest": {"minecraft:dandelion", "minecraft:poppy", "minecraft:lily_of_the_valley"}, "minecraft:forest": {"minecraft:dandelion", "minecraft:poppy", "minecraft:lily_of_the_valley"},
"minecraft:flower_forest": {"minecraft:dandelion", "minecraft:poppy", "minecraft:allium", "minecraft:azure_bluet", "minecraft:red_tulip", "minecraft:white_tulip", "minecraft:oxeye_daisy", "minecraft:cornflower"}, "minecraft:flower_forest": {"minecraft:dandelion", "minecraft:poppy", "minecraft:allium", "minecraft:azure_bluet", "minecraft:red_tulip", "minecraft:white_tulip", "minecraft:oxeye_daisy", "minecraft:cornflower"},
"minecraft:birch_forest": {"minecraft:dandelion", "minecraft:poppy"}, "minecraft:birch_forest": {"minecraft:dandelion", "minecraft:poppy"},
"minecraft:meadow": {"minecraft:dandelion", "minecraft:poppy", "minecraft:cornflower", "minecraft:allium"}, "minecraft:meadow": {"minecraft:dandelion", "minecraft:poppy", "minecraft:cornflower", "minecraft:allium"},
} }
// placeFlora scatters biome-appropriate small plants on grassy surface columns. // placeFlora scatters biome-appropriate small plants on grassy surface columns.
@ -117,8 +117,7 @@ func placeFlora(c *Chunk, r *chunkRand, surfTop *[16][16]int, grass *[16][16]boo
if c.GetBlock(lx, y, lz) != StateAir { if c.GetBlock(lx, y, lz) != StateAir {
continue continue
} }
flower := nameToStateID(flowers[int(r.next())%len(flowers)], nil) if flower, ok := nameToStateID(flowers[int(r.next())%len(flowers)], nil); ok {
if flower != StateAir {
c.SetBlock(lx, y, lz, flower) c.SetBlock(lx, y, lz, flower)
} }
} }
@ -155,8 +154,8 @@ func placeDesertFeatures(c *Chunk, r *chunkRand, surfTop *[16][16]int, biomeName
// placeCactus writes a 1-3 tall cactus column on top of the surface. // placeCactus writes a 1-3 tall cactus column on top of the surface.
func placeCactus(c *Chunk, lx, baseY, lz int, r *chunkRand) { func placeCactus(c *Chunk, lx, baseY, lz int, r *chunkRand) {
cactus := nameToStateID("minecraft:cactus", nil) cactus, ok := nameToStateID("minecraft:cactus", nil)
if cactus == StateAir { if !ok {
return return
} }
h := 1 + int(r.next()%3) h := 1 + int(r.next()%3)
@ -167,8 +166,8 @@ func placeCactus(c *Chunk, lx, baseY, lz int, r *chunkRand) {
// placeDeadBush writes a single dead_bush on the surface. // placeDeadBush writes a single dead_bush on the surface.
func placeDeadBush(c *Chunk, lx, baseY, lz int) { func placeDeadBush(c *Chunk, lx, baseY, lz int) {
db := nameToStateID("minecraft:dead_bush", nil) db, ok := nameToStateID("minecraft:dead_bush", nil)
if db == StateAir { if !ok {
return return
} }
c.SetBlock(lx, baseY, lz, db) c.SetBlock(lx, baseY, lz, db)
@ -200,8 +199,8 @@ func placeRocks(c *Chunk, r *chunkRand, surfTop *[16][16]int, grass *[16][16]boo
// placeBoulder writes a small 2-3 block cluster of stone-family blocks. // placeBoulder writes a small 2-3 block cluster of stone-family blocks.
func placeBoulder(c *Chunk, lx, baseY, lz int, r *chunkRand) { func placeBoulder(c *Chunk, lx, baseY, lz int, r *chunkRand) {
rocks := []string{"minecraft:cobblestone", "minecraft:granite", "minecraft:diorite", "minecraft:andesite"} rocks := []string{"minecraft:cobblestone", "minecraft:granite", "minecraft:diorite", "minecraft:andesite"}
block := nameToStateID(rocks[int(r.next())%len(rocks)], nil) block, ok := nameToStateID(rocks[int(r.next())%len(rocks)], nil)
if block == StateAir { if !ok {
return return
} }
n := 2 + int(r.next()%2) n := 2 + int(r.next()%2)

View file

@ -14,8 +14,8 @@ func TestHeightmapsDiffer(t *testing.T) {
c := NewChunk(0, 0, BiomePlains) c := NewChunk(0, 0, BiomePlains)
const floor = 64 const floor = 64
dandelion := nameToStateID("minecraft:dandelion", nil) dandelion, ok := nameToStateID("minecraft:dandelion", nil)
if dandelion == StateAir { if !ok {
t.Fatal("dandelion is missing from the block table") t.Fatal("dandelion is missing from the block table")
} }
for lx := 0; lx < 16; lx++ { for lx := 0; lx < 16; lx++ {
@ -96,8 +96,8 @@ func TestBlockStatePredicates(t *testing.T) {
} }
} }
// A flower is the case that separates WORLD_SURFACE from MOTION_BLOCKING. // A flower is the case that separates WORLD_SURFACE from MOTION_BLOCKING.
dandelion := nameToStateID("minecraft:dandelion", nil) dandelion, ok := nameToStateID("minecraft:dandelion", nil)
if dandelion == StateAir { if !ok {
t.Fatal("dandelion is missing from the block table") t.Fatal("dandelion is missing from the block table")
} }
if blocksMotionOrFluid(dandelion) { if blocksMotionOrFluid(dandelion) {
@ -113,11 +113,9 @@ func TestSectionFluidCount(t *testing.T) {
const y = 20 const y = 20
// One section: stone floor, water above it, and one waterlogged block — // One section: stone floor, water above it, and one waterlogged block —
// which counts as fluid even though it is not a fluid block. // which counts as fluid even though it is not a fluid block.
stairs := nameToStateID("minecraft:oak_stairs", map[string]string{ stairs, ok := nameToStateID("minecraft:oak_stairs", map[string]string{"waterlogged": "true"})
"facing": "north", "half": "bottom", "shape": "straight", "waterlogged": "true", if !ok {
}) t.Fatal("oak stairs are missing from the block table")
if stairs == StateAir {
t.Fatal("waterlogged oak stairs are missing from the block table")
} }
if stateFlags(stairs)&flagFluid == 0 { if stateFlags(stairs)&flagFluid == 0 {
t.Fatal("waterlogged stairs do not carry the fluid flag; the dump is wrong") t.Fatal("waterlogged stairs do not carry the fluid flag; the dump is wrong")

View file

@ -26,7 +26,7 @@ func TestIncrementalBlockLightAgainstVanillaFixture(t *testing.T) {
cache.chunkAt(cx, cz) cache.chunkAt(cx, cz)
} }
} }
glowstone := nameToStateID("minecraft:glowstone", nil) glowstone, _ := nameToStateID("minecraft:glowstone", nil)
if valid, _ := cache.SetBlockWithLight(15, 100, 8, glowstone); !valid { if valid, _ := cache.SetBlockWithLight(15, 100, 8, glowstone); !valid {
t.Fatal("glowstone edit rejected") t.Fatal("glowstone edit rejected")
} }
@ -67,7 +67,7 @@ func TestIncrementalBlockLightCrossesChunkBoundaryAndClears(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
glowstone := nameToStateID("minecraft:glowstone", nil) glowstone, _ := nameToStateID("minecraft:glowstone", nil)
if emission := lightEmission(glowstone); emission != 15 { if emission := lightEmission(glowstone); emission != 15 {
t.Fatalf("glowstone emission = %d, want 15", emission) t.Fatalf("glowstone emission = %d, want 15", emission)
} }

View file

@ -25,7 +25,7 @@ func TestSafeSpawnYRejectsUnderwaterColumn(t *testing.T) {
} }
func TestSafeSpawnYAcceptsNonOpaqueSolidFloor(t *testing.T) { func TestSafeSpawnYAcceptsNonOpaqueSolidFloor(t *testing.T) {
stairs := nameToStateID("minecraft:oak_stairs", nil) stairs, _ := nameToStateID("minecraft:oak_stairs", nil)
if stairs == StateAir { if stairs == StateAir {
t.Fatal("oak stairs state is unavailable") t.Fatal("oak stairs state is unavailable")
} }

View file

@ -3,6 +3,7 @@ package world
import ( import (
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"sort"
"strings" "strings"
"sync" "sync"
@ -27,6 +28,7 @@ var (
stateByIDOnce sync.Once stateByIDOnce sync.Once
stateByIDImpl map[uint16]stateName stateByIDImpl map[uint16]stateName
idsByName map[string][]uint16 idsByName map[string][]uint16
defaultByName map[string]uint16
) )
// stateByID returns the named form of a block-state ID, building the lookup // stateByID returns the named form of a block-state ID, building the lookup
@ -79,6 +81,7 @@ func buildStateTable() {
var blocks map[string]struct { var blocks map[string]struct {
States []struct { States []struct {
ID int `json:"id"` ID int `json:"id"`
Default bool `json:"default"`
Properties map[string]string `json:"properties"` Properties map[string]string `json:"properties"`
} `json:"states"` } `json:"states"`
} }
@ -87,6 +90,7 @@ func buildStateTable() {
} }
stateByIDImpl = make(map[uint16]stateName, 30000) stateByIDImpl = make(map[uint16]stateName, 30000)
idsByName = make(map[string][]uint16, len(blocks)) idsByName = make(map[string][]uint16, len(blocks))
defaultByName = make(map[string]uint16, len(blocks))
for name, b := range blocks { for name, b := range blocks {
for _, s := range b.States { for _, s := range b.States {
if s.ID < 0 || s.ID > 65535 { if s.ID < 0 || s.ID > 65535 {
@ -95,12 +99,20 @@ func buildStateTable() {
id := uint16(s.ID) id := uint16(s.ID)
stateByIDImpl[id] = stateName{Name: name, Properties: s.Properties} stateByIDImpl[id] = stateName{Name: name, Properties: s.Properties}
idsByName[name] = append(idsByName[name], id) idsByName[name] = append(idsByName[name], id)
if s.Default {
defaultByName[name] = id
}
} }
} }
} }
// blockPaletteEntry builds the NBT compound for a block-state ID: {Name, // blockPaletteEntry builds the NBT compound for a block-state ID: {Name,
// Properties} (Properties omitted when empty). Unknown IDs map to air. // Properties} (Properties omitted when empty). Unknown IDs map to air.
//
// Property keys are sorted. nbt.Compound preserves insertion order so that
// encoding is deterministic, but ranging a Go map is not: the same chunk saved
// twice produced different region-file bytes for every block with more than one
// property, which makes a byte-level diff of two saves useless.
func blockPaletteEntry(id uint16) *nbt.Compound { func blockPaletteEntry(id uint16) *nbt.Compound {
s, ok := stateByID(id) s, ok := stateByID(id)
if !ok { if !ok {
@ -108,9 +120,14 @@ func blockPaletteEntry(id uint16) *nbt.Compound {
} }
c := nbt.NewCompound().Set("Name", nbt.String(s.Name)) c := nbt.NewCompound().Set("Name", nbt.String(s.Name))
if len(s.Properties) > 0 { if len(s.Properties) > 0 {
keys := make([]string, 0, len(s.Properties))
for k := range s.Properties {
keys = append(keys, k)
}
sort.Strings(keys)
props := nbt.NewCompound() props := nbt.NewCompound()
for k, v := range s.Properties { for _, k := range keys {
props.Set(k, nbt.String(v)) props.Set(k, nbt.String(s.Properties[k]))
} }
c.Set("Properties", props) c.Set("Properties", props)
} }
@ -124,21 +141,47 @@ type paletteEntryKey struct {
sig string sig string
} }
// nameToStateID returns the block-state ID for a (name, properties) pair from // nameToStateID resolves a block name plus any properties to a state ID,
// the loaded table. It is used when decoding on-disk chunk NBT back into a // mirroring how vanilla reads a palette entry: start from the block's default
// Chunk. Unknown names/properties map to air (0). // state and apply the properties it recognises, keeping the default's value for
func nameToStateID(name string, props map[string]string) uint16 { // anything it does not.
//
// It used to return the block's *first* state — blocks.json lists states in
// StateDefinition.getPossibleStates() order, the property cartesian product,
// which has nothing to do with the default. For 642 of 1168 blocks those
// differ, so every caller passing nil got a corner state: redstone ore came out
// permanently lit, a sunflower came out as its own top half, oak stairs came out
// upside down and waterlogged. blocks.json marks the default state and the
// parser was dropping the flag.
//
// ok is false only for a name that is not a block at all.
func nameToStateID(name string, props map[string]string) (uint16, bool) {
stateByIDOnce.Do(buildStateTable) stateByIDOnce.Do(buildStateTable)
ids := idsByName[name] defaultID, ok := defaultByName[name]
for _, id := range ids { if !ok {
if propsMatch(stateByIDImpl[id].Properties, props) { return StateAir, false
return id }
if len(props) == 0 {
return defaultID, true
}
// Overlay only keys the block actually has; an unknown key or an illegal
// value leaves the default's value in place, which is what
// StateHolder.setValue's helper does after logging.
base := stateByIDImpl[defaultID].Properties
merged := make(map[string]string, len(base))
for k, v := range base {
if override, present := props[k]; present {
merged[k] = override
continue
}
merged[k] = v
}
for _, id := range idsByName[name] {
if propsMatch(stateByIDImpl[id].Properties, merged) {
return id, true
} }
} }
if len(ids) > 0 { return defaultID, true
return ids[0]
}
return StateAir
} }
func propsMatch(a, b map[string]string) bool { func propsMatch(a, b map[string]string) bool {

View file

@ -0,0 +1,91 @@
package world
import (
"bytes"
"testing"
"regionio/internal/nbt"
)
// TestNameToStateIDDefaults pins the one thing this function has to get right:
// a name with no properties resolves to the block's *default* state. It used to
// return blocks.json's first state, which is the property cartesian product's
// first entry and differs from the default for 642 of 1168 blocks.
func TestNameToStateIDDefaults(t *testing.T) {
cases := []struct {
name string
want uint16
why string
}{
{"minecraft:redstone_ore", 6882, "lit=false; the first state is lit=true, so every vein glowed"},
{"minecraft:sunflower", 12916, "half=lower; the first state is the top half of the plant"},
{"minecraft:oak_stairs", 3918, "north/bottom/straight/dry; the first state is top-half and waterlogged"},
{"minecraft:grass_block", StateGrass, "snowy=false, and it must agree with the StateGrass constant"},
{"minecraft:oak_log", StateOakLog, "axis=y, and it must agree with the StateOakLog constant"},
{"minecraft:oak_leaves", StateOakLeaf, "distance=7/persistent=false/dry, agreeing with StateOakLeaf"},
{"minecraft:stone", StateStone, "single state"},
{"minecraft:water", StateWater, "level=0"},
}
for _, c := range cases {
got, ok := nameToStateID(c.name, nil)
if !ok {
t.Errorf("%s: not found", c.name)
continue
}
if got != c.want {
t.Errorf("%s = %d, want %d (%s)", c.name, got, c.want, c.why)
}
}
if _, ok := nameToStateID("minecraft:not_a_block", nil); ok {
t.Error("an unknown block name resolved to a state")
}
}
// TestNameToStateIDOverrides checks the "default plus recognised overrides"
// behaviour on the path that matters — decoding a palette entry off disk.
func TestNameToStateIDOverrides(t *testing.T) {
full, ok := nameToStateID("minecraft:oak_stairs", map[string]string{
"facing": "east", "half": "top", "shape": "straight", "waterlogged": "true",
})
if !ok {
t.Fatal("oak stairs not found")
}
partial, ok := nameToStateID("minecraft:oak_stairs", map[string]string{
"facing": "east", "half": "top", "waterlogged": "true",
})
if !ok {
t.Fatal("oak stairs not found")
}
if full != partial {
t.Errorf("a partial property set gave %d, a complete one %d; the unnamed property should keep its default", partial, full)
}
// An unknown key and an illegal value both fall back to the default rather
// than to some unrelated corner state.
def, _ := nameToStateID("minecraft:oak_stairs", nil)
if got, _ := nameToStateID("minecraft:oak_stairs", map[string]string{"nonsense": "1"}); got != def {
t.Errorf("unknown property gave %d, want the default %d", got, def)
}
if got, _ := nameToStateID("minecraft:oak_stairs", map[string]string{"facing": "sideways"}); got != def {
t.Errorf("illegal property value gave %d, want the default %d", got, def)
}
}
// TestBlockPaletteEntryDeterministic guards the one genuinely non-deterministic
// thing in this file: the NBT Properties compound was filled by ranging a Go
// map, so saving the same chunk twice produced different region-file bytes.
func TestBlockPaletteEntryDeterministic(t *testing.T) {
stairs, ok := nameToStateID("minecraft:oak_stairs", nil)
if !ok {
t.Fatal("oak stairs not found")
}
encode := func() []byte {
return nbt.Marshal(nbt.NewCompound().Set("e", blockPaletteEntry(stairs)))
}
first := encode()
for i := 0; i < 64; i++ {
if got := encode(); !bytes.Equal(got, first) {
t.Fatalf("palette entry bytes differ between encodes on attempt %d:\n%x\n%x", i, first, got)
}
}
}

View file

@ -32,7 +32,7 @@ const dataVersion26 = 4790
// first time it ran: chunkAt prefers the store over the generator, so the // 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 // 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. // looks like it did nothing in exactly the place you are standing.
const generatorVersion = 7 const generatorVersion = 8
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced // generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
// because it is ours, not part of the vanilla chunk format. // because it is ours, not part of the vanilla chunk format.
@ -565,7 +565,9 @@ func readBlockStates(c *Chunk, si int, sc *nbt.Compound) {
} }
name := string(nbtAsString(ec, "Name")) name := string(nbtAsString(ec, "Name"))
props := readProps(ec) props := readProps(ec)
ids[i] = nameToStateID(name, props) // An unknown block name decodes to air rather than to a neighbour's
// state; that loses the block but does not corrupt the column.
ids[i], _ = nameToStateID(name, props)
} }
c.section(si) // ensure allocated c.section(si) // ensure allocated
s := c.sections[si] s := c.sections[si]

View file

@ -143,7 +143,7 @@ func TestStoreLightRoundTrip(t *testing.T) {
if _, err := cache.FrameErr(1, 0); err != nil { if _, err := cache.FrameErr(1, 0); err != nil {
t.Fatal(err) t.Fatal(err)
} }
glowstone := nameToStateID("minecraft:glowstone", nil) glowstone, _ := nameToStateID("minecraft:glowstone", nil)
if valid, _ := cache.SetBlockWithLight(15, 0, 8, glowstone); !valid { if valid, _ := cache.SetBlockWithLight(15, 0, 8, glowstone); !valid {
t.Fatal("glowstone edit rejected") t.Fatal("glowstone edit rejected")
} }
@ -191,7 +191,7 @@ func TestCacheReconcilesPersistedLightWithLoadedNeighbor(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
glowstone := nameToStateID("minecraft:glowstone", nil) glowstone, _ := nameToStateID("minecraft:glowstone", nil)
left := NewChunk(0, 0, BiomePlains) left := NewChunk(0, 0, BiomePlains)
left.SetBlock(15, 100, 8, glowstone) left.SetBlock(15, 100, 8, glowstone)
if err := store.SaveChunk(left); err != nil { if err := store.SaveChunk(left); err != nil {

View file

@ -48,7 +48,7 @@ func centreSurfaceBlock(ch *Chunk) (uint16, bool) {
st := s[blockIndex(8, MinY+i*16+ly, 8)] st := s[blockIndex(8, MinY+i*16+ly, 8)]
if st != StateAir && st != StateWater { if st != StateAir && st != StateWater {
// Dry only if this top block is at/above sea level. // Dry only if this top block is at/above sea level.
return st, (MinY+i*16+ly) >= SeaLevel return st, (MinY + i*16 + ly) >= SeaLevel
} }
} }
} }

View file

@ -4,6 +4,9 @@ import "testing"
func BenchmarkGenerateTerrain(b *testing.B) { func BenchmarkGenerateTerrain(b *testing.B) {
gen := NewTerrainGenerator(0) gen := NewTerrainGenerator(0)
b.ResetTimer(); b.ReportAllocs() b.ResetTimer()
for i := 0; i < b.N; i++ { _ = gen(int32(i), 0) } b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = gen(int32(i), 0)
}
} }

View file

@ -2,8 +2,8 @@ package world
import ( import (
"fmt" "fmt"
"testing"
"regionio/internal/worldgen" "regionio/internal/worldgen"
"testing"
) )
func TestTerrainHeightProfile(t *testing.T) { func TestTerrainHeightProfile(t *testing.T) {
@ -19,8 +19,12 @@ func TestTerrainHeightProfile(t *testing.T) {
} }
} }
line += fmt.Sprintf("%d ", top) line += fmt.Sprintf("%d ", top)
if top < minH { minH = top } if top < minH {
if top > maxH { maxH = top } minH = top
}
if top > maxH {
maxH = top
}
} }
t.Logf("surface heights (z=8): %s", line) t.Logf("surface heights (z=8): %s", line)
t.Logf("min=%d max=%d range=%d", minH, maxH, maxH-minH) t.Logf("min=%d max=%d range=%d", minH, maxH, maxH-minH)

View file

@ -1,7 +1,12 @@
package world package world
import "testing" import "testing"
func BenchmarkGenerateVanilla(b *testing.B){
g:=NewVanillaGenerator(12345) func BenchmarkGenerateVanilla(b *testing.B) {
b.ResetTimer(); b.ReportAllocs() g := NewVanillaGenerator(12345)
for i:=0;i<b.N;i++{ _=g(int32(i),0) } b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = g(int32(i), 0)
}
} }

View file

@ -4,8 +4,8 @@ import (
"encoding/json" "encoding/json"
"math" "math"
"os" "os"
"strings"
"strconv" "strconv"
"strings"
"testing" "testing"
) )
@ -33,10 +33,18 @@ func TestVanillaParity(t *testing.T) {
ourY := int(oh[idx]) - 65 ourY := int(oh[idx]) - 65
d := int(math.Abs(float64(ourY - vh[idx]))) d := int(math.Abs(float64(ourY - vh[idx])))
total++ total++
if d == 0 { exact++ } if d == 0 {
if d <= 1 { within1++ } exact++
if d <= 3 { within3++ } }
if d > maxDiff { maxDiff = d } if d <= 1 {
within1++
}
if d <= 3 {
within3++
}
if d > maxDiff {
maxDiff = d
}
} }
} }
pct := func(n int) float64 { return 100 * float64(n) / float64(total) } pct := func(n int) float64 { return 100 * float64(n) / float64(total) }