diff --git a/internal/world/store.go b/internal/world/store.go index 7bb3d00..b274177 100644 --- a/internal/world/store.go +++ b/internal/world/store.go @@ -32,7 +32,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 = 2 +const generatorVersion = 3 // generatorVersionTag is the NBT key holding generatorVersion. It is namespaced // because it is ours, not part of the vanilla chunk format. diff --git a/internal/world/vanilla.go b/internal/world/vanilla.go index 21d416c..2d9a2c0 100644 --- a/internal/world/vanilla.go +++ b/internal/world/vanilla.go @@ -273,18 +273,18 @@ func applySurfaceRule(out *[WorldHeight]uint16, wx, wz, seaLevel, minY int, biom Rng: colRng, } stoneDepthAbove := 0 - waterHeight := math.MinInt + waterHeight := worldgen.NoWaterAbove nextCeilingStoneY := math.MaxInt for i := top; i >= 0; i-- { y := minY + i old := out[i] if old == StateAir { stoneDepthAbove = 0 - waterHeight = math.MinInt + waterHeight = worldgen.NoWaterAbove continue } if isFluidState(old) { - if waterHeight == math.MinInt { + if waterHeight == worldgen.NoWaterAbove { waterHeight = y + 1 } continue diff --git a/internal/worldgen/surface.go b/internal/worldgen/surface.go index 2adcf8e..7a6a892 100644 --- a/internal/worldgen/surface.go +++ b/internal/worldgen/surface.go @@ -3,6 +3,7 @@ package worldgen import ( "encoding/json" "fmt" + "math" "math/rand" "sync" ) @@ -18,6 +19,11 @@ import ( // or the special bandlands badlands-clay rule. Condition tests are the 11 types // present in the overworld rule tree. +// NoWaterAbove is the "dry column" sentinel for SurfaceContext.WaterHeight, +// matching the Integer.MIN_VALUE vanilla uses. A caller building a context by +// hand must set it explicitly; the zero value would read as water at y=0. +const NoWaterAbove = math.MinInt + // SurfaceContext carries the per-block data a surface rule needs to decide. type SurfaceContext struct { // X, Y, Z are the block's world coordinates. @@ -30,7 +36,7 @@ type SurfaceContext struct { StoneDepthAbove int StoneDepthBelow int // WaterHeight is one above the lowest fluid block of the run of fluid - // directly above Y, or math.MinInt when no fluid sits above Y with no air + // directly above Y, or NoWaterAbove when no fluid sits above Y with no air // in between. It is what the water condition measures against. WaterHeight int // SeaLevel is the world sea level (63 for the overworld). @@ -148,19 +154,29 @@ type holeTest struct{} func (holeTest) Test(ctx *SurfaceContext) bool { return false } -// waterTest passes when the block is within `offset` of the water surface -// (vanilla SurfaceRules.WATER). We treat it as "at or just below sea level" — -// the common case for beach/shore rules. +// waterTest passes when the block is clear of the water above it — either there +// is none, or it sits far enough below the water's underside +// (SurfaceRules.WaterConditionSource). +// +// The height compared against is the column's own water surface, not sea level. +// Those differ wherever the aquifer put a pool at its own level: an underground +// lake, a mountain tarn or a flooded cave sit nowhere near y=63, and measuring +// them against sea level dressed dry stone as lakebed and lakebed as dry stone. type waterTest struct { - offset int - surfaceDepthMul int - addStoneDepth bool + offset int + surfaceDepthMul int + addStoneDepth bool } func (t waterTest) Test(ctx *SurfaceContext) bool { - // Vanilla: passes when Y >= seaLevel + offset + surfaceDepth*mul (±stone). - threshold := ctx.SeaLevel + t.offset + ctx.SurfaceDepth*t.surfaceDepthMul - return ctx.Y >= threshold + if ctx.WaterHeight == NoWaterAbove { + return true + } + y := ctx.Y + if t.addStoneDepth { + y += ctx.StoneDepthAbove + } + return y >= ctx.WaterHeight+t.offset+ctx.SurfaceDepth*t.surfaceDepthMul } // temperatureTest passes when the (column) temperature is below freezing — the diff --git a/internal/worldgen/surface_test.go b/internal/worldgen/surface_test.go index 40faa0a..4136afe 100644 --- a/internal/worldgen/surface_test.go +++ b/internal/worldgen/surface_test.go @@ -34,9 +34,11 @@ func TestSurfaceRuleNoPanic(t *testing.T) { for _, b := range biomes { for y := 0; y < 100; y++ { ctx := &SurfaceContext{ - X: 100, Y: y, Z: 100, StoneDepthAbove: 100 - y, + X: 100, Y: y, Z: 100, + StoneDepthAbove: 100 - y, StoneDepthBelow: y + 1, SeaLevel: 63, BiomeName: b, MinY: -64, - PreliminarySurface: 100, Rng: rand.New(rand.NewSource(1)), + PreliminarySurface: 100, WaterHeight: NoWaterAbove, + Rng: rand.New(rand.NewSource(1)), } rule.Apply(ctx) // must not panic } @@ -51,9 +53,10 @@ func TestSurfaceBedrockFloor(t *testing.T) { t.Fatalf("load: %v", err) } ctx := &SurfaceContext{ - X: 0, Y: -64, Z: 0, StoneDepthAbove: 0, + X: 0, Y: -64, Z: 0, StoneDepthAbove: 1, StoneDepthBelow: 1, SeaLevel: 63, BiomeName: "minecraft:plains", MinY: -64, - PreliminarySurface: 70, Rng: rand.New(rand.NewSource(1)), + PreliminarySurface: 70, WaterHeight: NoWaterAbove, + Rng: rand.New(rand.NewSource(1)), } state, ok := rule.Apply(ctx) if !ok { @@ -105,3 +108,64 @@ func TestIsColdBiome(t *testing.T) { } } } + +// TestWaterCondition pins SurfaceRules.WaterConditionSource against the +// column's own water surface. The offsets are the three forms the overworld +// tree actually uses: (0,0,false) for "is this block dry", (-1,0,false) for the +// block just under the waterline, and (-6,-1,true) for the beach/shore band. +func TestWaterCondition(t *testing.T) { + cases := []struct { + name string + test waterTest + ctx SurfaceContext + want bool + }{ + { + name: "no water above passes", + test: waterTest{}, + ctx: SurfaceContext{Y: 20, WaterHeight: NoWaterAbove}, + want: true, + }, + { + name: "at the waterline passes", + test: waterTest{}, + ctx: SurfaceContext{Y: 63, WaterHeight: 63}, + want: true, + }, + { + name: "one block under water fails", + test: waterTest{}, + ctx: SurfaceContext{Y: 62, WaterHeight: 63}, + want: false, + }, + { + name: "offset -1 reaches one block deeper", + test: waterTest{offset: -1}, + ctx: SurfaceContext{Y: 62, WaterHeight: 63}, + want: true, + }, + { + name: "an aquifer pool at y=-20 is measured against itself, not sea level", + test: waterTest{}, + ctx: SurfaceContext{Y: -25, WaterHeight: -20, SeaLevel: 63}, + want: false, + }, + { + name: "stone above the same pool is dry", + test: waterTest{}, + ctx: SurfaceContext{Y: -19, WaterHeight: -20, SeaLevel: 63}, + want: true, + }, + { + name: "add_stone_depth counts buried stone towards the threshold", + test: waterTest{offset: -6, surfaceDepthMul: -1, addStoneDepth: true}, + ctx: SurfaceContext{Y: 55, WaterHeight: 63, StoneDepthAbove: 2, SurfaceDepth: 0}, + want: true, + }, + } + for _, c := range cases { + if got := c.test.Test(&c.ctx); got != c.want { + t.Errorf("%s: got %v, want %v", c.name, got, c.want) + } + } +}