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.
This commit is contained in:
parent
0a2845fa76
commit
90e9380ae7
9 changed files with 224 additions and 606 deletions
132
CLAUDE.md
Normal file
132
CLAUDE.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# RegionIO — working notes
|
||||
|
||||
A Minecraft Java Edition server core in Go, targeting **26.1.2 / protocol 775**.
|
||||
The goal is vanilla fidelity, not a lookalike: where vanilla behaviour is known, match it exactly.
|
||||
|
||||
README.md describes what the server does. This file is about how to work on it.
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
go build ./... && go vet ./...
|
||||
make test # go test ./...
|
||||
make test-race # the race-sensitive subset
|
||||
make verify # both
|
||||
|
||||
go run ./cmd/regionio -seed 12345 # serves on 0.0.0.0:25565
|
||||
go run ./cmd/regionio -seed 12345 -world "" # in-memory world, nothing read from or written to disk
|
||||
go run ./cmd/gendump # client-free generator diagnostics
|
||||
```
|
||||
|
||||
## Hard rules
|
||||
|
||||
**No third-party dependencies.** `go.mod` has none. NBT, zlib framing, MD5 seeding, noise, the
|
||||
density-function interpreter — all in-tree. Keep it that way.
|
||||
|
||||
**Bump `generatorVersion` whenever the generator's output changes.** It lives in
|
||||
`internal/world/store.go` and is stamped into every saved chunk; a mismatch makes the chunk
|
||||
regenerate. Without a bump, the chunks already on disk keep their old terrain and your change looks
|
||||
like it did nothing in exactly the area you are standing in — `chunkAt` prefers the store over the
|
||||
generator. `TestGeneratorVersionStampRejectsStaleChunks` covers the mechanism. For quick iteration,
|
||||
`-world ""` sidesteps persistence entirely. (The seed is guarded separately, by the world metadata
|
||||
file, and a seed mismatch is a hard error rather than a regeneration.)
|
||||
|
||||
**Verify vanilla behaviour against the jar; don't recall it.** See below.
|
||||
|
||||
## Vanilla ground truth
|
||||
|
||||
None of these are redistributable, so all are gitignored. Obtain `server.jar` from Mojang.
|
||||
|
||||
| Source | What it gives you |
|
||||
|---|---|
|
||||
| `.refjava/` | Decompiled classes for the parts we port: `Climate`, `SurfaceRules`, `SurfaceSystem`, `Aquifer`, `DensityFunctions`, `NoiseRouterData`, `NoiseBasedChunkGenerator`, `TerrainProvider`, `PalettedContainer`, `LevelChunkSection`, `ClientboundLevelChunkPacketData` |
|
||||
| `versions/26.1.2/server-26.1.2.jar` | The real (deobfuscated) server. The outer `server.jar` is only a bundler |
|
||||
| `generated/reports/` | Datagen output: `blocks.json`, `registries.json`, `packets.json`, `biome_parameters/` |
|
||||
|
||||
The inner jar also carries the **complete worldgen datapack**, which is the source for everything we
|
||||
still approximate: 259 `placed_feature`, 222 `configured_feature`, 66 `biome` (with per-stage
|
||||
`features` and `carvers`), 5 `configured_carver`, 35 `structure`, 1359 structure NBTs — 648 KB for
|
||||
the feature/biome/carver set.
|
||||
|
||||
Three ways in, cheapest first:
|
||||
|
||||
```
|
||||
unzip -p versions/26.1.2/server-26.1.2.jar data/minecraft/worldgen/biome/plains.json
|
||||
javap -p -c -classpath versions/26.1.2/server-26.1.2.jar net.minecraft.world.level.chunk.Strategy
|
||||
```
|
||||
|
||||
`javap` settles questions the decompiled subset does not cover. It is how the biome palette
|
||||
threshold was pinned down: `Strategy$2` switches `{0..3}` and everything above falls through to the
|
||||
global palette, which `.refjava/` alone could not show.
|
||||
|
||||
When a constant has to come from vanilla's *runtime* rather than its source or reports, dump it with
|
||||
a throwaway Java program run against the jar. `tools/VanillaLightDump.java` is the precedent — it
|
||||
walks the block-state registry and emits opacity, emission and voxel face shapes into
|
||||
`internal/world/light_properties.bin`. Substring-matching block names is how the light table was
|
||||
wrong before (`grass_block` matched "grass", `bedrock` matched "bed"); don't reintroduce that shape
|
||||
of guess anywhere.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
cmd/regionio/ entry point (flags, listener, graceful shutdown)
|
||||
cmd/gendump/ client-free generator diagnostics — biome spread, surface blocks,
|
||||
subsurface banding, deep-layer composition, bedrock band, cross-section
|
||||
cmd/genblocks/ generates internal/worldgen/generated_blocks.go from the block report
|
||||
cmd/genlight/ legacy light-table generator, superseded by tools/VanillaLightDump.java
|
||||
tools/ Java dumpers run against the jar, plus their Go-side fixtures
|
||||
internal/protocol/ VarInt, framing, compression, packet IDs
|
||||
internal/nbt/ NBT codec (modified UTF-8)
|
||||
internal/registry/ 28 embedded synced registries + tags, verbatim from vanilla
|
||||
internal/world/ chunk model, wire encoder, cache + tickets, Anvil store, lighting,
|
||||
the chunk generator itself (vanilla.go), decoration
|
||||
internal/worldgen/ the library: noise, density-function interpreter, surface rules,
|
||||
climate/biome finder, embedded datapack under data/
|
||||
internal/network/ per-connection state machine
|
||||
internal/server/ shared core: config, sessions, status, profiles, entity loops
|
||||
```
|
||||
|
||||
Note the split: `internal/worldgen` is a *library* over the datapack; the chunk generator that drives
|
||||
it is `internal/world/vanilla.go`.
|
||||
|
||||
## Fidelity status
|
||||
|
||||
Bit-exact and parity-tested — treat as settled, change only with a vanilla reference in hand:
|
||||
`random.go` (Xoroshiro128++, `upgradeSeedTo128bit`, MD5 seeding), `improved_noise.go`,
|
||||
`perlin_noise.go`, `normal_noise.go`, `blended.go`, `spline.go`, `density.go`, the 4×8×4 cell grid
|
||||
with trilinear interpolation, the climate/biome finder, and the chunk wire encoder
|
||||
(`TestGoldenAgainstVanilla` compares bytes against a real vanilla chunk).
|
||||
|
||||
Known gaps, roughly in order of how visible they are:
|
||||
|
||||
- **Aquifers are absent.** `vanilla.go` floods every air block below sea level with water, so every
|
||||
cave below y=63 is solid water and there are no lava lakes. Vanilla resolves fluid per position
|
||||
through `Aquifer.computeSubstance`; the datapack ships all four router keys (`barrier`,
|
||||
`fluid_level_floodedness`, `fluid_level_spread`, `lava`) and they are not parsed.
|
||||
- **`above_preliminary_surface` is wrong**, so there is no subsurface banding: every land column is
|
||||
one grass block directly on stone, no dirt, no sandstone under sand. Vanilla is
|
||||
`blockY >= preliminarySurfaceLevel + surfaceDepth - 8`; we compare against the actual top block,
|
||||
which gates the whole biome surface subtree to a single block per column. `gendump` prints this.
|
||||
- **Several surface-rule conditions are stubs**: `hole` is hardcoded false (vanilla is
|
||||
`surfaceDepth <= 0`), `steep` is never assigned, `water` uses sea level instead of the column's
|
||||
real water height, and 6 of the 7 `noise_threshold` noises are unsupported so calcite, ice, packed
|
||||
ice, powder snow, swamp water and gravel patches never appear. `bandlands` is a 4-colour cycle
|
||||
rather than the 192-band array.
|
||||
- **No carvers and no ore veins.** Caves come only from the density router; `configured_carver` and
|
||||
the `vein_*` router keys are unimplemented.
|
||||
- **Decoration is hand-written heuristics**, not the vanilla feature system: oak trees only and
|
||||
without a biome check (so oaks grow in deserts), ores that cannot generate below y≈0 because they
|
||||
only replace stone and never deepslate, and no grass, flowers, lakes or springs.
|
||||
|
||||
## Testing worldgen
|
||||
|
||||
`make verify` is the gate, but most generator defects are invisible to it — they show up as terrain
|
||||
that looks wrong. `cmd/gendump` exists for that: biome distribution, top surface blocks, subsurface
|
||||
banding, deep-layer composition, the bedrock band, and an ASCII cross-section, with no client
|
||||
involved. Add an assertion to it whenever you fix a class of defect; the bedrock-band check is the
|
||||
model — it prints per-layer counts and fails loudly on any air or water in the floor.
|
||||
|
||||
`internal/world/vanilla_parity_test.go` compares surface heights against a capture from the official
|
||||
server and skips when the capture is absent. Note it reads a hardcoded `/tmp` path, so on Windows it
|
||||
never runs. A capture is produced by running the vanilla server headless at a known seed and reading
|
||||
its region files back with our own `regionfile.go` + `nbt`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue