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).
Chunk generation and sending no longer block the read loop. The read
loop pushes a non-blocking recenter request and stays free to handle
movement, chat, and keep-alive acks immediately; a per-connection
streamer goroutine generates chunks in a worker pool and sends them
serially under the write mutex.
- network/streamer.go: per-connection streamer. spiralOrder emits
chunks centre-outward; parallelSend fans Cache.Frame across a worker
pool (Cache.Frame is already goroutine-safe), parallelGenerate warms
a one-ring predictive border so movement finds ready chunks; the
loaded-set is owned solely by the streamer.
- network/play.go: beginPlay launches the streamer and pushes an
initial recenter instead of the old blocking streamAround;
onPlayerMove now just calls requestRecenter (non-blocking).
- network/handler.go: ctx (connection lifetime) + streamer field; the
streamer stops when the read loop ends (cancel on serve exit).
- network/configuration.go: client view_distance is saved (clamped
2..16) and drives the streamer radius.
- Tests: spiral order (centre-first, ring structure) and the
non-blocking recenter guarantee the read loop relies on.
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.
Replaces the biome-blind fillVanillaColumn heuristics with a full
interpreter for the overworld surface_rule tree (already embedded in
overworld.json): block/sequence/condition/bandlands rules plus all 11
condition tests (biome, steep, hole, water, temperature, y_above,
stone_depth, noise_threshold, not, vertical_gradient,
above_preliminary_surface).
- worldgen/blockids.go: name(+Properties)→network-ID table for surface
blocks (grass/sand/terracotta/mycelium/podzol/coarse_dirt/sandstone/
calcite/snow/ice/...), with snowy property variants.
- worldgen/surface.go: rule-tree parser + interpreter + SurfaceContext;
LoadOverworldSurfaceRule caches the seed-independent tree.
- loader.go: OverworldDensity.SurfaceRule() exposes the parsed tree.
- biome_lookup.go: BiomeNameAt returns the biome name for biome tests.
- vanilla.go: samples the 2D climate + biome before column fill, threads
the rule tree and biome name into fillVanillaColumn, and applies it
top-down with stone as the default for non-matching (deeper) blocks.
The above_preliminary_surface gate uses an inclusive bound so the top
solid block reaches the biome dispatch.
- Performance: one per-column RNG and a reused SurfaceContext keep the
overhead to ~+13ms/chunk (71ms vs 58ms baseline), within the gate.
- Chunk stores per-section biome arrays (64 cells/section); flat generators
keep the uniform single-valued fallback.
- New writeBiomePalette uses min 1 bpe and direct at registry width (65 biomes).
- Climate sampler splits 2D axes (sampled once per column) from 3D depth
(per cell), keeping per-cell cost to a single density-function compute.
- Full biome parameter table (surface + underground twins + lush/dripstone/
deep_dark caves) with depth as a true range, not a binary layer.
- fillBiomes3D fills the 1536 cells/chunk in parallel; <0.3ms overhead vs
baseline chunk gen (benchmark-verified).
- Tests: cave-biome resolution, per-cell variation, flat-world regression,
registry-range validity, plus chunk-gen and per-cell benchmarks.