From c10719cb78db049c0f1510037f337076826e8331 Mon Sep 17 00:00:00 2001 From: Master290 Date: Mon, 27 Jul 2026 02:05:20 +0300 Subject: [PATCH] Measure the surface water condition against the column's own water The water condition asked "is this block at or above sea level", which is only the same question as vanilla's in an ocean. Vanilla asks how far the block sits below the water directly above it, and there is now water that is nowhere near y=63: the aquifer puts pools at their own levels, deep underground and up in the hills. Against sea level every one of those read as dry stone, and the stone above them read as lakebed. waterHeight is already tracked down the column, so the condition becomes the vanilla one: pass when there is no water above at all, otherwise when blockY (+ stoneDepthAbove where the rule asks for it) clears waterHeight + offset + surfaceDepth * multiplier. add_stone_depth was parsed and then ignored; three rules in the overworld tree set it. NoWaterAbove replaces a bare math.MinInt so a hand-built context cannot default to "water at y=0" by leaving the field unset. --- internal/world/store.go | 2 +- internal/world/vanilla.go | 6 +-- internal/worldgen/surface.go | 36 +++++++++++----- internal/worldgen/surface_test.go | 72 +++++++++++++++++++++++++++++-- 4 files changed, 98 insertions(+), 18 deletions(-) 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) + } + } +}