Commit graph

7 commits

Author SHA1 Message Date
Master290
c19e5f0e4f Bind the surface rule tree to the world seed
The tree was parsed once, globally, and shared by every world -- so every
condition that needs the seed simply did not work. Compiling it per RandomState
fixes four of them at once.

noise_threshold sampled a per-column random draw and pretended it was
"minecraft:surface"; the other six noises it names were unsupported and returned
false. Each condition now holds its own seeded noise, sampled once per column
into a small cache the way vanilla's LazyXZCondition does. Powder snow, packed
ice and ice appear in the dump for the first time; calcite, swamp water windows
and gravel patches have their conditions back too.

vertical_gradient tapered through a per-column RNG shared with the other rules.
Vanilla rolls a positional random at the exact block, from a factory named by
the rule. More importantly the anchor decoder read only above_bottom and
discarded which kind of anchor it was, so the deepslate rule's absolute 0..8
collapsed onto y=-64 and **no deepslate existed anywhere in the world**. Anchors
now carry their kind and resolve against the real height bounds -- which also
retires a hardcoded 384 in y_above.

Two more stubs land with them: hole is surfaceDepth <= 0 rather than a constant
false, and steep reads the neighbouring column heights. steep needs the whole
chunk's heightmap, so the column pass is now two passes -- terrain and fluids
for all 256 columns, then surface rules -- which is the order vanilla uses
anyway (doFill, then buildSurface).

Deepslate was also missing from the block-ID table, and an unknown name resolved
to 0, which the caller read as "no block" and skipped. So even a correct rule
would have placed nothing. Unknown names are now a parse error, deepslate and
mud are in the table, and a rule that resolves to air genuinely places air --
the frozen-ocean surface asks for exactly that.

Below y=0 is now entirely deepslate, y=1..7 a scatter, above y=8 none.
2026-07-27 02:25:09 +03:00
Master290
1083e47211 Restore the subsurface layers: real above_preliminary_surface and surface depth
Every land column was one block of grass sitting straight on stone. No dirt
under grass, no sandstone under sand, nothing. Two stubs did it together:

above_preliminary_surface compared blockY against the column's actual top block,
so of every position in the column exactly one passed -- and the entire
biome-specific half of the surface rule tree hangs under that condition.
Vanilla compares against a minimum surface level: the preliminary surface level
sampled at the four corners of the 16-block cell, bilinearly interpolated, plus
the surface depth less 8. That is about twenty blocks of reach on ordinary
terrain, which is what the biome subtree is written against.

Surface depth was hardcoded to 0. Vanilla is surfaceNoise*2.75 + 3 with a
per-column jitter, so it comes out around three; it sets how thick the band is
and feeds every add_surface_depth term in the tree. Zero collapsed them all.

Also samples surface_secondary, so stone_depth's secondary_depth_range widens
its band instead of being parsed and dropped.

Grass columns now read grass, two to four dirt, stone -- the histogram over 256
columns is {2: 223, 3: 33}, against vanilla's 2..4. gendump prints it and fails
if the band collapses again; TestGrassColumnsHaveDirt guards it in the suite.
2026-07-27 02:15:14 +03:00
Master290
c10719cb78 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.
2026-07-27 02:05:20 +03:00
Master290
21a10ab65e Implement the vanilla Aquifer; stop flooding caves
Every air block below y=63 was turned into water. That is one line of code and
it cost the entire underground: no dry caves, no lava lakes, no air pockets, a
solid block of water from the sea floor to bedrock.

Vanilla decides fluid per position instead. Aquifer centres sit on a jittered
16x12x16 grid; each gets a fluid level and type from the floodedness and spread
noises, with centres near open sky inheriting the sea and buried ones getting a
much lower randomised level or nothing at all. A position takes its nearest
centre's fluid unless the barrier noise raises enough pressure between the two
or three nearest centres to seal it back to stone. Deep centres turn to lava.

Porting it means fixing the order of generation, not just adding a file. Vanilla
resolves stone/water/lava/air during the density pass and only then runs the
surface rules over a finished column; we did it the other way round, which is
what forced the unconditional flood in the first place. fillVanillaColumn now
asks the aquifer per position, and applySurfaceRule walks the finished column
carrying the bookkeeping SurfaceSystem carries: air resets the counters, a fluid
records its water height, and stone gets a depth from the top of its run plus
one from the bottom, found by looking ahead to the next non-stone block below.

That last one fixes stone_depth's ceiling form, which had no bottom-up depth to
work with and was testing the top-down one instead -- fourteen rules in the
overworld tree use it to dress cave roofs. The floor form is unchanged: vanilla
counts from 1 and compares against 1 + offset, we counted from 0 and compared
against offset.

The aquifer grid is built eagerly per chunk rather than lazily, because our
columns fill concurrently; every cell is a pure function of its grid coordinate
and every cell in the computed range gets consulted anyway. Cost is ~0.5% of
chunk generation, most of it absorbed by the shared preliminary-surface cache.

Inland caves go from 100% water to 3.8%, and lava exists for the first time.
cmd/gendump grows a census that would have failed loudly before, and
TestCavesAreDry guards it in the suite.
2026-07-27 02:02:12 +03:00
Master290
90e9380ae7 Stop the saved world from masking generator changes
chunkAt prefers the store over the generator, and nothing invalidated a stored
chunk when the generator changed. Combined with four region files committed to
the repo, the chunks around spawn were frozen output from an older generator:
worldgen fixes looked like they did nothing in exactly the area you land in when
you join, and a fresh clone inherited that world.

Chunks now carry a RegionIOGeneratorVersion stamp, written on save and checked on
load; a mismatch returns ErrChunkNotFound so the caller regenerates. Chunks
written before the stamp existed have no tag, decode as 0, and are invalidated
the same way. Bump the constant in any commit that changes generator output.

This is deliberately per-chunk and deliberately quiet. The world metadata file
already guards the seed, where a mismatch means two incompatible terrains and
refusing to open is right. A generator change is routine by comparison and should
just regenerate.

world/region/*.mca and chat.md are untracked (left on disk) and /world/ is
gitignored, superseding the narrower /world/regionio-world.json rule, along with
/.refjava/ and root-level session transcripts.

CLAUDE.md covers the parts of working here that README does not: the
no-dependencies rule, the version-stamp rule and why forgetting it looks like a
failed fix, where the vanilla ground truth lives and how to query the jar
directly with unzip and javap, the precedent for dumping runtime constants with a
throwaway Java program, and an honest list of which layers are bit-exact versus
approximated.
2026-07-27 01:28:05 +03:00
Master290
cae06eb97e Implement multiplayer persistence and vanilla lighting 2026-07-21 09:21:44 +03:00
Master290
d1cc29bb60 Chunk persistence to Anvil .mca region files
The world now survives restarts: chunks load from disk (read-through
cache) and player edits persist via async autosave + a final SaveAll on
shutdown. RegionIO finally does region I/O.

- world/regionfile.go: Anvil .mca container — 8192-byte header
  (offset + timestamp tables), 4096-byte sectors, zlib chunk records.
- world/compress.go: zlib deflate/inflate for chunk payloads.
- world/store.go: chunk <-> Level-nested NBT (per-section
  block_states/biomes palettes, WORLD_SURFACE heightmap, DataVersion
  4790, yPos -4) via the existing nbt package; Store opens one
  RegionFile per region with proper floor-division coords.
- world/state_names.go: id->name bridge from the embedded blocks.json
  report so network int-IDs round-trip through the disk named palette.
- world/encode.go: GetBiome read accessor for serialization.
- world/cache.go: read-through (disk then generation), dirty tracking,
  StartAutosave (returns a done channel so the saver exits before
  Close), SaveAll, NewCacheWithStore.
- server.go + main.go: Config.WorldDir (default "world"), -world flag,
  autosave loop every 30s, SaveAll + store Close on signal.
- Tests: region round-trip/absent/overwrite, chunk NBT round-trip,
  end-to-end save-reload, negative chunk coords, autosave persistence.
2026-06-25 00:40:14 +03:00