bedrockAt had two bugs that cancelled into a deterministic, wrong-looking floor.
It took its chunkRand by value, so next() mutated a copy and all four layers
drew the same 32-bit number. The layers were then decided by successive bits of
that one draw, nesting them into a prefix condition instead of scattering them
independently.
Its ramp also ran backwards. The comment claimed d=1 -> 50% decaying upward, but
`keep := 5 - d` requires more bits set the *lower* the layer, giving 1/16 at the
floor and 1/2 four blocks up — bedrock was likelier further from the bottom.
Vanilla ramps probability linearly from 1 at y=-64 to 0 at y=-59 and tests
nextFloat() < probability, which is what it does now.
Only fillLegacySurface reaches this; the normal path lets the surface rule tree
place the floor from the same datapack vertical_gradient rule. Both should agree.
cmd/gendump is new here: a client-free diagnostic that reports biome
distribution, top surface blocks, subsurface banding, deep-layer composition and
an ASCII cross-section, so generator defects can be seen without launching a
client. Its bedrock-band check prints per-layer counts and fails on any air or
water in the floor. On chunk (0,0) at seed 12345 it now reports y=-64 fully
bedrock, 207/154/106/66 thinning above it, and zero air or water.
The same output also shows the missing subsurface banding — grass sits directly
on stone — which is a separate defect in above_preliminary_surface, not fixed
here.
The in-memory chunk+frame cache no longer grows unbounded as players
explore. An LRU policy (doubly-linked list + index map, O(1) per op)
evicts least-recently-used chunks when the cache exceeds MaxCachedChunks,
dropping both the chunk and its cached frame. Dirty chunks are skipped
until the autosave flushes them, so no edit is ever lost to eviction.
- world/cache.go: maxChunks field + order/index LRU bookkeeping; touch
(move-to-front) on every chunkAt/Frame/SetBlock hit; evictIfNeeded on
miss; NewCacheWithLimit constructor (0 = unbounded, backward-compat).
Dirty chunks are bumped to MRU and left in place rather than doing
region I/O under the cache mutex.
- server/server.go: Config.MaxCachedChunks (default 1024); New wires it
into NewCacheWithLimit when a world dir is set.
- cmd/regionio/main.go: -maxcache flag.
- Tests: limit cap, LRU ordering (touched chunk survives), both-maps
drop, dirty-keep, reload-on-access, and edits-survive-eviction+reload
(end-to-end via the store).
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.