Commit graph

110 commits

Author SHA1 Message Date
51b4891f81 Diagnose ore feature seed indices 2026-08-17 01:37:40 +03:00
cb60050801 Match vanilla ore sphere math 2026-08-17 01:14:24 +03:00
c68c5c2744 Match vanilla ore block traversal 2026-08-17 00:45:32 +03:00
400e76382a Report fixture ore schedules 2026-08-11 14:01:40 +03:00
e2f496fafc Measure cross-chunk ore replay 2026-08-11 13:50:14 +03:00
9ffd2b19ba Place scheduled ores across regions 2026-08-11 13:35:47 +03:00
d01e576dcc Schedule features from mutable regions 2026-08-11 13:23:45 +03:00
4ff62d7c96 Evaluate world-aware block predicates 2026-08-11 13:13:02 +03:00
1a921434d9 Model mutable feature regions 2026-08-11 13:01:52 +03:00
ea66091f85 Schedule source chunk features globally 2026-08-11 12:47:59 +03:00
11440f556c Execute world-aware placement modifiers 2026-08-11 12:33:35 +03:00
053c5bec6a Execute placement modifiers in order 2026-08-11 09:55:50 +03:00
674d0192c9 Match vanilla placement height sampling 2026-08-11 09:42:11 +03:00
e9fe526300 Pin feature ordering to vanilla runtime 2026-08-11 09:29:17 +03:00
a73b689449 Use world coordinates for ore placement 2026-08-11 01:15:34 +03:00
a23650fc0b Match vanilla ore ellipsoid geometry 2026-08-11 00:56:53 +03:00
b11e6f14cb Pin biome fitness against vanilla runtime 2026-08-10 23:59:22 +03:00
be8f030698 Commit exhaustive vanilla worldgen fixture 2026-08-10 23:54:04 +03:00
064f0f1cca Drive basic trees from biome feature stages 2026-08-10 23:36:12 +03:00
2bfa3155f3 Place biome-driven water and lava springs 2026-08-10 23:29:20 +03:00
dd2ea56850 Load vanilla feature stages and place ores 2026-08-10 23:20:43 +03:00
28c11e2fc2 Bound connection outbound queues 2026-08-10 23:00:20 +03:00
ca019756ec Add vanilla parity harness and harden server boundaries 2026-08-10 22:54:16 +03:00
1924cb5591
Add GNU AGPL v3 license
Added the GNU Affero General Public License version 3 to the project.
2026-08-10 22:25:42 +03:00
Master290
c6185d88c8 Configured carvers: caves and canyons
The router's noise caves are one kind of cave. The other kind -- the long
winding tunnels with rooms and side branches, and the ravines that cut down
through the terrain -- is walked, step by step, by a random source, and none of
it existed.

The shape of the work is unusual enough to state plainly. To carve one chunk,
vanilla replays every carver seeded in the 17x17 chunks around it and keeps only
what lands inside, so the same tunnel is walked up to 289 times across a world.
That redundancy is the point: it is what lets a chunk be carved without
generating its neighbours, which is the only way carving fits a generator that
produces one chunk at a time. A carve-once-write-into-neighbours design would be
cheaper and would not reproduce vanilla's mask and ordering.

Two primitives had to be right before any of it could be, and both are pinned
against values captured from the jar:

  * setLargeFeatureSeed, which decides which chunks start a cave. It combines
    its two products with XOR; setDecorationSeed, which it otherwise resembles,
    uses addition and forces the low bit. Getting them the wrong way round moves
    every tunnel in the world and nothing complains.
  * Mth.sin and Mth.cos, which are a 65536-entry lookup table and not libm.
    Mth.sin(-1.0) is -0.8414514 against Math.sin's -0.8414709848078965, and a
    tunnel that walks by adding cos(yaw) a hundred times ends up somewhere else
    entirely if that difference is smoothed away.

Carving lands between the surface pass and decoration, where vanilla puts it,
and both neighbours matter: the surface rules must already have placed grass for
a cave mouth to be retextured, and decoration must come after so nothing is
planted over a hole. The heights decoration plants against are recomputed
afterwards, which is why vanilla re-primes its heightmaps at the start of the
feature step.

The configs are extracted from the jar rather than transcribed, along with the
flattened #minecraft:overworld_carver_replaceables tag, so the probabilities and
Y ranges are data. Open volume below y=60 rises 28% over sixteen sampled chunks,
tunnels cut at or below y=-56 fill with lava rather than air (869 blocks, no
air), and the cost is inside the noise floor of the density pass.
2026-07-27 03:58:56 +03:00
Master290
0f76058db6 Ore veins from the noise router
The router has carried vein_toggle, vein_ridged and vein_gap since the whole of
it was parsed, and nothing read them. So the copper and iron mega-veins -- the
long branching sheets that run through the deepslate and the copper band, not
the small scattered ore blobs -- did not exist.

OreVeinifier is not a decoration feature. It is the second entry of the same
MaterialRuleList the aquifer heads: the aquifer answers first, and only where it
says the position is solid rock does the veinifier get a turn at what would
otherwise be plain stone. That is why a vein never opens into a cave, and it is
why this lands in the density pass rather than in decorate.

Three random draws per position, in a fixed order, from a factory hashed off
"minecraft:ore": solidness, then richness, then the rare raw-ore block.
Reordering them or hoisting the vein_gap compute above the second draw would
change every vein in the world, so the code follows the bytecode's order rather
than the one that reads better.

The Y windows are the sharp edge worth knowing about: a vein's type comes from
the sign of the veininess noise, but its window comes from the type, so
veininess <= 0 anywhere outside -60..-8 is simply not a vein. Over 49 sampled
chunks copper lands in 0..49 and iron in -57..-8, and raw ore comes out at 1.9%
of ore blocks against vanilla's 2%.
2026-07-27 03:48:48 +03:00
Master290
e0fdddd887 Write chunk NBT vanilla reads: flat root, byte section Y, four-bit block palettes
Our region files were not Anvil. Three things stood between them and vanilla's
deserializer, and each is enough on its own:

  * everything was nested under a "Level" compound. Chunk data lived there until
    1.18; SerializableChunkData builds a flat root and never looks for the key.
  * a section's Y was an Int. Vanilla writes putByte and reads getByteOr, so
    every section of ours decodes as index 0 and overwrites the one before it.
  * block palettes were packed at ceil(log2(size)) bits. Strategy's tableswitch
    routes bit counts 1 through 4 to the same four-bit configuration, so a
    palette of 2..16 states is four bits wide on disk. Ours were one to three,
    which makes the long array a quarter of the length vanilla computes, and
    SimpleBitStorage rejects the section outright rather than misreading it.

Biome containers were already right: Strategy has no such floor for them, and a
Global configuration above three bits still stores palette indices, just at its
own width. The suspicion that biomes collapsed on save/load was unfounded --
what let it stand is that every round-trip test in this package set blocks and
asserted blocks, so nothing proved biomes survived. They do now, per cell, for
every palette width a section can hold.

Verified against vanilla rather than against our reading of it:
tools/VanillaChunkFormatCheck.java opens a region file we wrote using vanilla's
RegionFile, NbtIo, Strategy and SimpleBitStorage. Sixteen generated chunks pass.
Reverting either the Y type or the palette floor makes it fail with vanilla's
own message -- "Invalid length given for storage, got: 64 but expected: 256" --
so the check can fail, which is the only reason to trust it passing.

Reading a world the official server generated is what the surface-height parity
capture in CLAUDE.md has always needed, and this is half of it.
2026-07-27 03:44:48 +03:00
Master290
113a59e365 Resolve block names to the default state, not the first one
blocks.json lists a block's states in getPossibleStates() order -- the property
cartesian product -- and separately marks which one is the default. The parser
declared only id and properties, so the default flag was dropped on the floor
and nameToStateID returned states[0]. Those differ for 642 of the 1168 blocks.

What that produced, measured rather than guessed: every redstone vein in the
world was lit=true and glowing, every sunflower was placed as its own top half
with nothing under it, and oak stairs came out upside down and waterlogged. It
also quietly disagreed with the StateGrass, StateOakLog and StateOakLeaf
constants next door in encode.go, which are the real defaults.

Now it starts from the default state and applies the properties it recognises,
keeping the default's value for an unknown key or an illegal value -- which is
what vanilla does when it reads a palette entry. That matters on the disk path:
a chunk written with a property we no longer know used to decode to a random
corner state instead of something sane.

The signature grows an ok result, because air was doing double duty as both a
real block and "no such name".

Separately, blockPaletteEntry filled the Properties compound by ranging a Go
map. nbt.Compound preserves insertion order precisely so encoding is
deterministic, so saving one chunk twice produced different region-file bytes
for every multi-property block. Keys are sorted now.
2026-07-27 03:35:39 +03:00
Master290
9e91425c5d Write the section fluid count instead of a hardcoded zero
LevelChunkSection puts two shorts in front of every section: nonEmptyBlockCount
and fluidCount. We wrote the first and then a literal 0 for the second, under a
comment claiming it was a reserved field that vanilla always leaves at zero.
It is not reserved and vanilla does not.

So every client was told every section is fluid-free, in a world where the
aquifer now fills oceans, lakes and flooded caves. The golden test did not catch
it because its fixture is a superflat chunk whose real fluid count is zero.

The count is per block state, not per block: a waterlogged stair holds a fluid
while a dry one does not, and the flag for that comes from the block-state dump
added with the heightmaps.
2026-07-27 03:28:49 +03:00
Master290
7880531bdb Send three real heightmaps instead of one repeated three times
writeHeightmaps computed "highest non-air" once and wrote the same 37 longs
under all three ids, on the stated assumption that our terrain has no leaves or
transparency. That stopped being true the moment the generator grew trees and
flowers.

Vanilla's three client heightmaps stop at different blocks: WORLD_SURFACE at the
first thing that is not air, MOTION_BLOCKING at the first that blocks motion or
holds fluid, MOTION_BLOCKING_NO_LEAVES at the first such thing that is not a
LeavesBlock -- an instanceof, not the minecraft:leaves tag. The client places
rain and snow particles off MOTION_BLOCKING and lands a fishing bobber on it, so
a tree canopy reported as solid ground rains under itself.

Neither blocksMotion() nor the leaves test is derivable from blocks.json: the
first reads cached VoxelShape collision geometry and the forceSolidOn/Off
properties, the second is a Java class check. So the Java dumper grows three
flag bits and the whole thing is renamed for what it now is -- block state
properties, not just lighting. tools/VanillaBlockStateDump.java writes
internal/world/block_properties.bin at format 2; the light bytes are unchanged
byte for byte and only the previously unused high flag bits moved.

Verified the dumper round trip while doing it: recompiling the old
VanillaLightDump against the jar reproduces the committed binary exactly, so the
data really does come from the runtime registry and not from a stale checkout.
CLAUDE.md now carries the command to rebuild it.
2026-07-27 03:27:09 +03:00
Master290
57214fbd76 Run the world clock so day and night happen
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.
2026-07-27 02:45:28 +03:00
Master290
4db7638377 Update the fidelity notes for the surface pass
The gap list described a surface pipeline that no longer exists: no subsurface
banding, surfaceDepth pinned at zero, six unsupported noises, no deepslate.
What is left is smaller and more specific — carvers, ore veins,
PerlinSimplexNoise, and the two SurfaceSystem extensions that run outside the
rule tree.

Also records the two things that are now compile-time errors rather than silent
wrong output: a surface rule naming a block the ID table does not have, and the
rule tree being used without a seed.
2026-07-27 02:32:05 +03:00
Master290
3a255b52e1 Real badlands clay bands and a real biome temperature table
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.
2026-07-27 02:31:10 +03:00
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
e77fe3dc07 Update the fidelity notes for the aquifer
The gap list still said aquifers were absent and that the water condition used
sea level, and it did not mention deepslate never being placed -- which is the
loudest thing gendump prints now that the fluids are right.
2026-07-27 02:06:45 +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
ed045ee09d Parse aquifer and ore-vein noise routers
The overworld noise_router ships fourteen keys; we read six. The eight left on
the floor are exactly the ones the aquifer, the ore veins and the preliminary
surface estimate need, so every one of those subsystems has been impossible to
write.

Wire the rest of the router into OverworldDensity: barrier,
fluid_level_floodedness, fluid_level_spread and lava for the aquifer,
vein_toggle/vein_ridged/vein_gap for the veins, and preliminary_surface_level
for both. Two node types were missing and are added with them --
minecraft:invert (the reciprocal, not negation: Mapped.Type ordinal 5 is
1.0/input) and minecraft:find_top_surface, which walks down from an upper bound
in cell_height steps looking for positive density.

PreliminarySurfaceLevelAt wraps that node the way NoiseChunk does: quart-align
the column, then memoise. The cache is per generator rather than per chunk
because the aquifer samples columns up to three chunks away, so neighbours
overlap heavily -- with a shared cache a chunk costs a few dozen evaluations
instead of a few thousand.

Also lifts sea_level, min_y, height and the aquifers/ore-veins flags out of the
settings file, and adds PositionalRandomFactory.At for the aquifer cell centres
(Mth.getSeed hashed into the low half of the factory seed).

No generator output changes yet: nothing reads the new keys.
2026-07-27 01:44:45 +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
0a2845fa76 Fix the bedrock floor ramp and add a gendump check for it
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.
2026-07-27 01:28:04 +03:00
Master290
c399070f59 Write biome palettes in the form the client actually reads
Vanilla's SECTION_BIOMES palette strategy switches on the bit count with
`tableswitch {0..3}`: 0 single-valued, 1-3 linear, and everything else falls
through to the global palette. There is no hashmap tier for biomes — that exists
only for block states, whose 0..8 switch we already implement correctly.

We were writing a linear palette all the way up to 7 bits. A section holding 9 or
more distinct biomes therefore went out as a 4-bit indirect container while the
client read it as global: no palette prefix consumed, long array re-read at 7
bits, and every field after it in the chunk payload misaligned. Sections that
straddle the surface and the cave biomes really do carry that many, so this is
reachable in ordinary terrain rather than a corner case.

Checked against the jar rather than recalled: javap -c on Strategy$2 shows the
{0..3} switch with Configuration$Global in the default arm.

The test decodes each container the way the client would and requires it to
consume exactly the bytes we produced, so a misframed container shows up as a
byte count instead of needing a client to notice. Restoring the old threshold
fails the 9-, 20- and 65-biome cases.
2026-07-27 01:28:04 +03:00
Master290
4453bdeb55 Gate entities on client chunk residency 2026-07-21 11:25:24 +03:00
Master290
2b07d6be20 Fix terrain streaming and surface spawning 2026-07-21 11:11:19 +03:00
Master290
f0279cdb65 Fix add entity velocity encoding 2026-07-21 10:30:28 +03:00
Master290
6413216876 Fix default spawn packet layout 2026-07-21 10:22:31 +03:00
Master290
cbd5c7b546 Implement ticketed chunk lifecycle 2026-07-21 10:04:56 +03:00
Master290
cae06eb97e Implement multiplayer persistence and vanilla lighting 2026-07-21 09:21:44 +03:00
Master290
8f7cacf9d9 Fix structures tearing apart and floating due to inconsistent baseY calculations 2026-06-27 21:54:08 +03:00
Master290
788983dc88 Add StructureManager and NBT template loader for large structures 2026-06-27 21:28:11 +03:00