-featureless derives a world datapack from the server jar (inner server
or bundler layout): every biome keeps its carvers but loses its feature
stages and every structure set loses its structures, so the capture is
pure noise terrain with surface rules, carvers, aquifers, and veins.
-blocks-only writes a blocks-only RIOBASE1 fixture for it. -port avoids
colliding with something else already listening on 25565.
set_time was declared nowhere and sent never, so the client's sky was frozen
wherever it started: the sun did not move, night did not fall, and nothing in
the world had a time.
26.1.2 replaced the old (gameTime, dayTime, doDaylightCycle) triple with a
registry of named clocks. The packet now carries a fixed-width game time plus a
map from world clock to (totalTicks, partialTick, rate); the client advances
each clock locally at its rate and drives the minecraft:day timeline -- a
keyframe track over a 24000-tick period -- from the overworld clock. The clock
registry is already among the 28 we sync, so the id is looked up rather than
hardcoded and the server refuses to start if it is missing.
The counter rides the entity tick loop because that is the only loop already
running at 20 TPS. It belongs on the single authoritative tick the engine still
needs; putting a sixth ticker beside the five that exist would make that worse.
Broadcast every second, which is what vanilla does -- the client interpolates in
between, so the resend only corrects drift.
The clock also persists now. The world metadata file was written once and never
touched again; it is atomically rewritable, carries gameTime and dayTime, and a
file written before those fields existed still opens and resumes at dawn as it
did. Saved every 30 seconds alongside the chunk autosave, and once more on
shutdown after the final flush.
The bandlands rule cycled four terracotta colours off a per-column random draw.
Vanilla generates a 192-entry band table once per world, from a random source
named clay_bands, and reads it at the block's height shifted by the
clay_bands_offset noise. Brown, red and light grey terracotta were never placed
anywhere; the stripes were the wrong thickness and did not line up between
neighbouring columns. All seven colours now appear.
The temperature condition matched a hand-written list of eleven biome names.
Replacing it with the temperature field read out of the jar's 65 biome JSONs
fixes one of them: deep_frozen_ocean reads cold by name but its base
temperature is 0.5, so vanilla does not freeze it. taiga and the pine taigas
were the other way round -- excluded by name, and correctly so, but by
coincidence rather than by data.
Two parts of the vanilla calculation are left out and documented where they
belong: the height adjustment that cools peaks, and the "frozen" modifier that
warms scattered patches of frozen ocean. Both need PerlinSimplexNoise. Neither
is reachable from the overworld tree in a way that shows: the single condition
that consults temperature sits under a frozen_ocean biome check, below a water
check, and decides whether a hole in the ocean floor ices over. The snowy
mountain tops come from biome selection, not from here -- which is not what the
plan for this commit assumed.
The per-column *rand.Rand threaded through SurfaceContext goes away with the
old bandlands rule; nothing needs it now that vertical_gradient rolls
positionally.
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.
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.
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.
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.