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
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -16,10 +16,22 @@
|
||||||
# All build-time embeds live under internal/; generated/ is not needed to build.
|
# All build-time embeds live under internal/; generated/ is not needed to build.
|
||||||
/generated/
|
/generated/
|
||||||
|
|
||||||
|
# Decompiled vanilla sources kept locally for reference (not redistributable).
|
||||||
|
/.refjava/
|
||||||
|
|
||||||
|
# Generated world. Chunks are reproducible from the seed, and committing them is
|
||||||
|
# actively harmful: the store takes priority over the generator, so a stale
|
||||||
|
# region file pins old terrain around spawn and hides worldgen changes. This
|
||||||
|
# supersedes the narrower /world/regionio-world.json rule.
|
||||||
|
/world/
|
||||||
|
|
||||||
# Logs and runtime artefacts
|
# Logs and runtime artefacts
|
||||||
/logs/
|
/logs/
|
||||||
*.log
|
*.log
|
||||||
/world/regionio-world.json
|
|
||||||
|
# Development notes and session transcripts kept locally, not source.
|
||||||
|
/chat.md
|
||||||
|
/20[0-9][0-9]-*.txt
|
||||||
|
|
||||||
# Editor / tool local config
|
# Editor / tool local config
|
||||||
/.claude/
|
/.claude/
|
||||||
|
|
|
||||||
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`.
|
||||||
605
chat.md
605
chat.md
|
|
@ -1,605 +0,0 @@
|
||||||
проанализируй проект
|
|
||||||
Worked for 1m 55s
|
|
||||||
Теперь у меня достаточно информации для всестороннего анализа. Вот его результаты.
|
|
||||||
|
|
||||||
Анализ проекта RegionIO
|
|
||||||
RegionIO — это серверная реализация Minecraft Java Edition, написанная на Go. Проект нацелен на актуальную версию игры (26.1.2, протокол 775) и воспроизводит полный путь подключения: от статуса в списке серверов до реальной генерации мира с помощью ванильного алгоритма плотности (density function).
|
|
||||||
|
|
||||||
1. Назначение и архитектура
|
|
||||||
Проект — это с нуля написанный сервер MC Java (не мод, не прокси). Он поддерживает:
|
|
||||||
|
|
||||||
пинг статуса сервера (MOTD/пинг) и согласование handshake;
|
|
||||||
автономный вход (offline mode): создаёт offline UUID, идентификатор игрока;
|
|
||||||
фазу конфигурации: доставляет бренд, флаги компонентов (feature flags), реестры (registries) и теги (tags), а также согласование known-packs;
|
|
||||||
фазу игры: стриминг чанков (chunks), блочная модификация, творческий инвентарь, размещение/разрушение блоков, чат и keep-alive.
|
|
||||||
Слоистая архитектура (пакет internal/)
|
|
||||||
Пакет Назначение
|
|
||||||
protocol Константы, идентификаторы пакетов, VarInt/VarLong, типы, Reader/Writer, фрейминг, сжатие
|
|
||||||
nbt Кодировщик/декодер NBT (включая mutf8)
|
|
||||||
registry Встроенные ванильные синхронизированные реестры 26.1.2 + теги
|
|
||||||
world Представление чанков, level_chunk_with_light кодировщик, кэш, генераторы (ванильный/плоский), постоянство элементов
|
|
||||||
worldgen Шумовое ядро (Perlin/Improved/Blended/Normal), движок density-function, парсер загрузчика datapack
|
|
||||||
network Listener + машина состояний handler для каждого соединения (handshake→status/login→configuration→play)
|
|
||||||
server Config, общий стейт, ответ на статус в формате JSON
|
|
||||||
Один поток выполнения на каждое соединение (go l.serveConn); состояние каждого handler’а является потокобезопасным, так как он принадлежит goroutine цикла чтения.
|
|
||||||
|
|
||||||
2. Ключевые сильные стороны
|
|
||||||
Правдоподобная ванильная генерация мира. world.NewVanillaGenerator считывает настоящий noise_router.final_density из встроенного датапака (overworld.json, ~111 КБ) и строит дерево плотностных функций через worldgen.Loader. Узлы Interpolated семплируются на сетке ячеек 4×8×4 и трилинейно интерполируются (как настоящий NoiseChunk), а final_density вычисляется поблочно (world/vanilla.go:36, worldgen/density.go). Это кропотливое, побитовое воссоздание, не упрощённый шум — это амбициозный инженерный выбор.
|
|
||||||
|
|
||||||
Аккуратная обработка протокола. encode.go эмитирует контейнеры с палитрами (single-valued/indirect/direct карты палитр), packed long arrays, карты высот (карта высот с 9 битами/7 на long), а также лайтмаску скайлайта. protocol имеет разумные пределы (MaxPacketSize 2 МБ, MaxVarIntLen и т.д.) для защиты от вредоносного ввода.
|
|
||||||
|
|
||||||
Производительность через кэширование. world.Cache хранит кадр level_chunk для каждого чанка, готовый к сжатию; изменение блока инвалидирует только кадр этого чанка (cache.go:92). Генерация вынесена за пределы блокировки мьютекса (mutex), чтобы избежать блокировок при стриминге.
|
|
||||||
|
|
||||||
Чёткое управление состоянием. Реализована полная последовательность ванильного подключения: select_known_packs → registry_data (has_data=false) → update_tags → finish_configuration. Пакет registry документирует важность порядка отправки реестра.
|
|
||||||
|
|
||||||
Параллелизм при генерации. generateVanilla вычисляет сетку углов ячеек и заполнение колонок через sync.WaitGroup (goroutine на ось X).
|
|
||||||
|
|
||||||
3. Ограничения и пробелы
|
|
||||||
Существующие функции
|
|
||||||
|
|
||||||
Только офлайн-режим (login.go:18) — нет шифрования Mojang/Microsoft, нет проверки session.
|
|
||||||
Нет реального освещения. light.go эмитирует фиксированный «полностью яркий» скайлайт и пустой блочный лайт. Это работает, но не позволяет создавать caves/затемнённые области.
|
|
||||||
Нет персистентности мира. Cache хранит чанки только в памяти; после перезапуска мир регенерируется из сида. Нет сохранения/загрузки .mca.
|
|
||||||
Стриминг чанков фиксированный. chunkRadius = 4 жёстко задан (play.go:23); не учитывается реальное client_information.view_distance. Чанки, выходящие за радиус, отправляются повторно только при перемещении центра.
|
|
||||||
Нет полного стейта игрока/сущностей. Отслеживается только креативный хотбар и положение телепорта; нет HP, голода, инвентаря, сущностей, мобов, дропа предметов.
|
|
||||||
Чат локальный. handleChat эхо-возвращает только отправителю (play.go:240); нет реестра игроков/трансляции.
|
|
||||||
Биомы упрощены. Весь чанк использует единый биом (BiomePlains) через контейнер с одним значением на секцию.
|
|
||||||
Возможные проблемы в коде
|
|
||||||
|
|
||||||
WorldSeed из Config никогда не устанавливается — DefaultConfig() возвращает его как нулевое значение, поэтому каждый сервер генерирует мир с одним и тем же сидом 0 (server.go:26, main.go:20).
|
|
||||||
conn.go/frame.go — стоит проверить handleUseItemOn: порядок чтения полей insideBlock/worldBorderHit чувствителен к версии протокола и был источником тонких багов рассинхронизации при размещении блоков.
|
|
||||||
nbt.Marshal(nbt.String(text)) в sendSystemChat — эхо сообщений является однопользовательским; чат по протоколу ожидает подписанные компоненты (здесь смягчено через enforcesSecureChat=false, что верно, но заслуживает внимания при масштабировании).
|
|
||||||
4. Структура данных
|
|
||||||
server.jar (60 МБ) — это настоящий ванильный сервер 26.1.2, хранящийся рядом, по-видимому, в качестве эталона/источника для дампов (libraries/, versions/26.1.2/ — это распакованные ассеты Mojang, используемые для создания synced_registries.json, тегов, отчётов по блокам/предметам).
|
|
||||||
generated/reports/ — вывод ванильного --generateReports, ставший источником для item_blocks.json, данных о блок-стейтах и т.д.
|
|
||||||
internal/worldgen/data/ — встроенный экстракт датапака, управляемый загрузчиком.
|
|
||||||
.claude/settings.local.json — предлагает, что проект разрабатывался при участии ИИ-ассистента.
|
|
||||||
Тесты всесторонние: golden_test.go, vanilla_parity_test.go (битовая точность против ванильного стандарта), бенчмарки для шума/плотности/генерации.
|
|
||||||
|
|
||||||
5. Краткое содержание
|
|
||||||
RegionIO — это высококачественный, ambitiously-точный клон сервера Minecraft на Go, который демонстрирует глубокое понимание протокола 26.1.2 и ванильного генератора шума. Он проходит дальше по стеку, чем большинство «учебных» серверов (настоящая воспроизводимая по битам местность, креативное размещение блоков, фаза конфигурации), но находится на ранней стадии с точки зрения геймплея: офлайн-логин, освещение без истинного распространения, мир только в памяти, без сущностей/инвентаря/мобов. Естественные следующие этапы — это (а) сделать сид настраиваемым, (б) honour view distance, (в) базовый реестр игроков с широковещательной передачей чата, и (г) начать работу над настоящим движком освещения или персистентностью.
|
|
||||||
|
|
||||||
Хотите, чтобы я углубился в какую-то конкретную часть (например, проверил корректность размещения блоков, проверил паритет ванильного шума или обрисовал, как добавить сохранение чанков)?
|
|
||||||
|
|
||||||
а вообще сложно будет перенести прям ванильную генерацию
|
|
||||||
Worked for 1m 25s
|
|
||||||
Теперь у меня полная картина того, что уже есть и чего не хватает. Дам развёрнутый ответ.
|
|
||||||
|
|
||||||
Насколько сложно перенести «прям ванильную» генерацию
|
|
||||||
Хорошие новости: самое сложное — шумовое ядро и final_density — в проекте уже сделано, и сделано серьёзно. Плохие новости: «ванильный мир», который видит игрок, — это не только final_density, это большой конвейер из ~15 шагов поверх него. Разложу по слоям сложности.
|
|
||||||
|
|
||||||
Что УЖЕ есть (это ~40% пути, но самые мучительные 40%)
|
|
||||||
Компонент Статус
|
|
||||||
RandomState (Xoroshiro + fromHashOf через MD5) ✅ точная реализация
|
|
||||||
ImprovedNoise / PerlinNoise / BlendedNoise / NormalNoise ✅ с parity-тестами
|
|
||||||
Парсер density_function JSON → дерево узлов ✅ (loader.go)
|
|
||||||
Все нужные DF-ноды: add/mul/min/max/abs/square/cube/squeeze/clamp/y_clamped_gradient/noise/shifted_noise/shift_a/shift_b/old_blended_noise/weird_scaled_sampler/spline/interpolated/range_choice ✅
|
|
||||||
Трилинейная интерполяция на cell-grid 4×8×4 ✅ (vanilla.go)
|
|
||||||
Встроенный overworld.json (noise_router из 26.1.2) ✅
|
|
||||||
То есть форма рельефа (континенты, горы, глубины) уже генерится бит-совместимо с ванильным final_density. Это именно та часть, на которую у авторов "с нуля" серверов уходят месяцы. Здесь она уже за вас сделана.
|
|
||||||
|
|
||||||
Чего НЕ хватает — по возрастанию сложности
|
|
||||||
Слой 1. Дешёвые (дни–неделя)
|
|
||||||
1. Биомы (3D, не плоские). Сейчас весь чанк — BiomePlains (один single-valued контейнер). В ванилле биом — это отдельный paletted container по той же сетке 4×4×4 (biome_size=4), который вычисляется через climate::findTopBiome — спарс temperature/humidity/continentalness/erosion/weirdness + маппинг multi_noise. Для этого нужны:
|
|
||||||
|
|
||||||
парсер biome JSON (параметры temperature, humidity, offset, weirdness);
|
|
||||||
алгоритм findTopBiome (R-tree / квадрат расстояния search);
|
|
||||||
таблица параметров ванильных биомов (есть в датапаке).
|
|
||||||
Это рутинная, но объемная работа — порядка 800–1500 строк. Данные у вас уже есть (overworld.json включает всё noise_router).
|
|
||||||
|
|
||||||
2. Сид из конфига. Сейчас WorldSeed никогда не задаётся (DefaultConfig() возвращает 0). Это однострочник в main.go, просто отмечу как пробел.
|
|
||||||
|
|
||||||
3. Поверхностные блоки (surface rules). Сейчас песок/трава/земля накидываются эвристикой в fillVanillaColumn. В ванилле — это SurfaceSystem с правилами из worldgen/surface_rule (камень → трава в plains, песок у воды, терракоты в badlands, и т.д.). Это конечный автомат по столбцу; ~300–500 строк + данные.
|
|
||||||
|
|
||||||
Слой 2. Средние (недели)
|
|
||||||
4. Пещеры. final_density уже включает в себя noodle/spaghetti/peaks-caves как отрицательные значения плотности — то есть где-то они уже вырезаны. Но в ванилле есть ещё NoiseRouter отдельные ноды: spaghetti_roughness, pillars, noodle (с собственным cache и cave_in условиями). Часть из них в дереве есть и считается (loader.go их парсит), но хвосты вроде noodle_cave требуют специфичной логики.
|
|
||||||
|
|
||||||
5. Расположение структур. Это отдельный большой зверь. StructureManager с размещением структур (structure placement) (текселы, разделители, экземпляры), StructureSets (placement_concentric_rings для крепостей, placement_random_spread). Самые заметные для игрока — деревни, аванпосты, крепости (Strongholds). Реально сложно только из-за объёма и того, что форматы структур (.nbt) надо парсить и масштабировать (jigsaw/block-list с pallete).
|
|
||||||
|
|
||||||
6. Руды и мелкие фичи (decorator / placed_feature). Vanilla генерация — это сотни размещаемых фич (placed_feature), применяемых по биомам с биом-специфичными фильтрами. Каждая — это JSON-конфигурация + кодек для формы (ore, disk, tree, flower...). Реалистично — это тысячи строк и большой набор данных. Хороший порядок: сначала только ore + tree (oak/birch), потом расширять.
|
|
||||||
|
|
||||||
Слой 3. Сложные (месяцы)
|
|
||||||
7. Освещение. Сейчас это «full bright» (light.go). Ванильный мир без тёмных пещер выглядит плоско, а мобы/рост растений зависят от света. Реальное распространение света (sky light + block light, BFS по чанкам с обновлениями по границам) — это отдельный под-движок, ~1500–3000 строк, и производительно сделать его сложно.
|
|
||||||
|
|
||||||
8. Сохранение (MCA/Region file). Сейчас всё в памяти (Cache). Ванильные region/.mca с компрессией Zlib, chunk-NBT (включая Heightmap, Light, block_entities, sections с palette) — это ещё ~1000–2000 строк на чтение/запись. Название «RegionIO» как бы намекает, что это в планах, но сейчас не реализовано.
|
|
||||||
|
|
||||||
9. Жидкости (waterlogging, flow). Сейчас вода — статичный блок. Ванильная гидродинамика (liquid tick, flowing_water с уровнями) — отдельный движок.
|
|
||||||
|
|
||||||
10. Сущности (mob spawning) и блок-сущности (chest, sign, ...). Для «играбельного» мира нужно много всего: AI мобов, инвентарь, дроп, боевая система. Это уже не генерация, а полноценный геймплей.
|
|
||||||
|
|
||||||
Реалистичная оценка по этапам
|
|
||||||
Цель Усилия Результат
|
|
||||||
Сид из конфига + починить эвристики поверхности день мир выглядит «как ванильный» по форме
|
|
||||||
3D-биомы (multi_noise) 1–2 недели правильные биомы в чанке
|
|
||||||
Surface rules из датапака неделя правильные блоки поверхности
|
|
||||||
Пещеры/ore/tree placed_features 2–4 недели узнаваемые шахты и леса
|
|
||||||
Сохранение в .mca 2–3 недели мир переживает рестарт
|
|
||||||
Освещение 3–6 недель тёмные пещеры, ночь
|
|
||||||
Структуры (хотя бы деревни) 1–2 месяца узнаваемые объекты
|
|
||||||
До «настоящего ванильного мира, который можно играть» — это месяцы работы (3–6+ для одного человека в темпе энтузиаста), но до «мира, который генерится неотличимо от ванильного по рельефу и биомам» — реально 1–2 месяца, потому что фундамент уже стоит.
|
|
||||||
|
|
||||||
Главный практический вывод
|
|
||||||
Вы не пишете генератор с нуля — вы достраиваете конвейер поверх уже работающего final_density-ядра. Самая «неподъёмная» часть (шум + density-tree + random_state) за вас сделана. Это означает, что любая следующая задача — это в основном:
|
|
||||||
|
|
||||||
добавить недостающий DF-узел (если парсер на него ругается);
|
|
||||||
написать surface/biome/feature слой, читающий те же встроенные JSON;
|
|
||||||
покрыть паритет-тестом против server.jar, который лежит рядом.
|
|
||||||
Если хотите, могу оценить и спланировать первый реальный шаг — например, добавить 3D-биомы или сделать мир из реального сида. Скажите, какой слой вам интереснее, и я разберу его по конкретным файлам/функциям в ванильном коде.
|
|
||||||
|
|
||||||
Да, давай
|
|
||||||
Worked for 6m 6s
|
|
||||||
Теперь у меня полная картина. Дам предметный план по выбранному шагу (реальный сид + фикс поверхности), с конкретикой по файлам.
|
|
||||||
|
|
||||||
План: реальный сид + фикс поверхности
|
|
||||||
Что у вас уже есть
|
|
||||||
В worldgen/data/noise/ лежат все нужные шумы, включая surface.json, surface_secondary.json, surface_swamp.json, badlands_surface.json, iceberg_surface.json.
|
|
||||||
В generated/reports/biome_parameters/ — полная таблица параметров биомов.
|
|
||||||
Но surface_rule (самое важное для этого шага) в датапаке нет — в ванилле он живёт в dimension JSON (overworld.json на верхнем уровне, не noise_router). Ваш встроенный overworld.json, видимо, вырезан до одного noise_router. Это ключевой момент: либо тащить surface_rule из server.jar, либо писать эвристику, не претендующую на полную ванильность.
|
|
||||||
Декомпозиция задачи
|
|
||||||
Часть A. Реальный сид (тривиально, ~30 минут)
|
|
||||||
1. server.DefaultConfig() → добавить поле со значением по умолчанию + флаг из окружения. internal/server/server.go:26 — добавить WorldSeed: <value> (например 42, как заметный сид). Сейчас WorldSeed объявлен, но всегда 0.
|
|
||||||
|
|
||||||
2. cmd/regionio/main.go — чтение сида из аргумента/окружения. Сейчас main.go:20 просто зовёт server.DefaultConfig(). Добавить чтение REGIONIO_SEED (или -seed флаг), парсинг в int64, и проброс в Config.WorldSeed.
|
|
||||||
|
|
||||||
Это уже даст разные миры — генератор сид-зависимый с самого начала (RandomState через NewXoroshiro(seed)), просто сейчас всегда seed=0.
|
|
||||||
|
|
||||||
⚠️ Нюанс: OfflineUUID(name) от сида не зависит, так что.UUID игроков не поменяется.
|
|
||||||
|
|
||||||
Часть B. Фикс поверхности — главный вопрос объёма
|
|
||||||
Текущая логика в fillVanillaColumn (vanilla.go:92):
|
|
||||||
|
|
||||||
|
|
||||||
text
|
|
||||||
y == MinY → bedrock
|
|
||||||
solid[i]:
|
|
||||||
beach && i > top-4 → sand
|
|
||||||
i == top && y >= SeaLevel → grass
|
|
||||||
i > top-4 → dirt
|
|
||||||
else → stone
|
|
||||||
y < SeaLevel, not solid → water
|
|
||||||
Это разумное приближение для plains, но расходится с ваниллой в нескольких местах:
|
|
||||||
|
|
||||||
Расхождение Ванильное поведение Где чинить
|
|
||||||
Bedrock-слой: у вас 1 блок Ванилла: 1 слой у y=MinY + случайные bedrock до y=MinY+4 (через noise: surface или hardcoded pattern) fillVanillaColumn
|
|
||||||
Dirt под травой: у вас 3 блока Ванилла (SurfaceRules Mountain/Default): 3 блока dirt, но на глубине → stone. У вас совпадает ✅ —
|
|
||||||
Песок у воды: ваша эвристика top >= 0 && topY < SeaLevel Ванилла: песок по StoneShore/Beaches правилу через surface_rule, ширина пляжа зависит от surface_secondary шума эвристика (ок) или surface_rule
|
|
||||||
Grass vs Mycelium/Podzol: только plains В biome-aware версии — разные блоки по биомам отложить до 3D-биомов
|
|
||||||
Snow на высотах: нет temperature шум + высота → снег сверху отложить (нужен temperature-семплинг)
|
|
||||||
Вода в пещерах (aquifer): нет aquifer_* шумы + lava в глубоких отложить (Layer 2)
|
|
||||||
Рекомендация по части B — два варианта
|
|
||||||
Вариант B1 (быстрый, 1 день): доработать эвристику. Без surface_rule, просто сделать поверхность «правдоподобнее»:
|
|
||||||
|
|
||||||
Bedrock-слой: y ∈ [MinY, MinY+4] → bedrock с убывающей вероятностью через chunkRand (как делает decorate).
|
|
||||||
Подправить песок: пляж только если topY ∈ [SeaLevel-3, SeaLevel+1] (сухой берег), а не вся подводная часть. Сейчас слишком много песка на морском дне.
|
|
||||||
Добавить gravel на глубокой воде (через gravel шум, который уже есть в датапаке).
|
|
||||||
Вариант B2 (правильный, 3–5 дней): вытащить surface_rule из server.jar. server.jar лежит рядом. surface_rule в ванилле — это дерево правил (Sequence/Condition/Bandlands/Mountain/StoneShore...), сериализованное через SurfaceRules.Codecs. Что нужно:
|
|
||||||
|
|
||||||
Распаковать из server.jar настоящий data/minecraft/dimension/overworld.json (или .nbt), взять оттуда surface_rule.
|
|
||||||
Написать парсер правил → SurfaceRule-дерево (по образцу loader.go).
|
|
||||||
Написать SurfaceSystem.applySurface(column, biome, noise) — конечный автомат по столбцу.
|
|
||||||
Это заметно больше кода (~600–1000 строк), но даёт «настоящую» ванильную поверхность: терракоты в badlands, коренные породы, правильные пляжи. Но без 3D-биомовCondition-правила (проверяющие биом) не сработают полностью — будут работать только depth/steepness/noise-based правила.
|
|
||||||
|
|
||||||
Мой совет
|
|
||||||
Сделайте A + B1 сейчас (полдня), а B2 отложите до 3D-биомов — тогда surface rules раскроются полностью. Комбинировать «сид из конфига + аккуратная поверхность» имеет смысл как самостоятельный, законченный шаг: вы получите воспроизводимые миры, которые выглядят чище, и заодно подготовите тест-инфраструктуру (parity-тест против разных сидов).
|
|
||||||
|
|
||||||
Хотите, чтобы я:
|
|
||||||
|
|
||||||
Реализовал A + B1 прямо сейчас (сид из env/флага + фикс bedrock/песок/гравий в fillVanillaColumn + обновление parity-теста под сид из конфига)?
|
|
||||||
Или сначала расписал B2 пофункционально — какой exactly SurfaceRule-кодек из ваниллы и как его переложить в Go?
|
|
||||||
1
|
|
||||||
Worked for 3m 46s
|
|
||||||
Готово. Вот сводка по шагу A + B1.
|
|
||||||
|
|
||||||
Что сделано
|
|
||||||
Часть A — реальный сид
|
|
||||||
internal/server/server.go: WorldSeed теперь явно инициализируется в DefaultConfig() (0 по умолчанию, с комментарием).
|
|
||||||
cmd/regionio/main.go: добавлен флаг -seed и переменная окружения REGIONIO_SEED (приоритет: флаг > env > default). Формат — decimal или 0x hex (strconv.ParseInt с base 0). Невалидное значение env не молчит, а логирует ошибку и откатывается к умолчанию (чтобы опечатка не породила «другой мир»).
|
|
||||||
Часть B1 — фикс поверхности (internal/world/vanilla.go)
|
|
||||||
Bedrock-слой: вместо 1 блока у пола теперь ванильный паттерн — сплошной bedrock на MinY + убывающая вероятность на слоях MinY+1..MinY+4 (≈50/25/12.5/6.25 %). Реализовано через детерминированный per-column PRNG newColumnRand(wx, wz, seed) (новый хелпер рядом с newChunkRand), так что паттерн стабилен и не зависит от порядка горутин.
|
|
||||||
Песок: пляж теперь узкая полоса у уреза воды (SeaLevel-3 ≤ topY ≤ SeaLevel+1), а не вся подводная часть. Раньше морское дно сплошь покрывалось песком.
|
|
||||||
Гравий: на глубоком морском дне (topY < SeaLevel-3) верхний блок — gravel вместо песка.
|
|
||||||
Проверка
|
|
||||||
go build ./... — OK
|
|
||||||
go vet ./... — чисто
|
|
||||||
go test ./... — все тесты проходят (nbt, world, worldgen). Golden-тест проверяет GenerateFlat (отдельный путь) — не затронут. Parity-тесты используют публичный NewVanillaGenerator(seed) — сигнатура не менялась.
|
|
||||||
Как запустить
|
|
||||||
|
|
||||||
text
|
|
||||||
regionio -seed 12345
|
|
||||||
# или
|
|
||||||
set REGIONIO_SEED=12345 && regionio
|
|
||||||
Замечание / что дальше
|
|
||||||
Это эвристика (B1), а не настоящий surface_rule-конвейер ваниллы. Песок/гравий/bedrock теперь выглядят правдоподобнее, но:
|
|
||||||
|
|
||||||
биом-зависимые поверхности (mycelium в mushroom_fields, терракота в badlands, снег в горах) невозможны без 3D-биомов;
|
|
||||||
настоящий bedrock-floor в ванилле считается через BedrockBlockSource/noise-функцию, а не PRNG-аппроксимацией.
|
|
||||||
Логичный следующий шаг — 3D-биомы (multi_noise), после которого surface_rule (B2) раскроется полностью. Сказать, когда будете готовы?
|
|
||||||
|
|
||||||
|
|
||||||
3 files changed
|
|
||||||
+87 -10
|
|
||||||
да
|
|
||||||
Worked for 17m 25s
|
|
||||||
Готово. Этап 1 (per-column биомы) завершён.
|
|
||||||
|
|
||||||
Что сделано
|
|
||||||
Новые файлы
|
|
||||||
internal/worldgen/biome.go — воспроизведение ванильного Climate: квантование (round(v*10000)), TargetPoint, fitDistance (сумма квадратов разностей по 6 осям), BiomeParameter с half-open диапазонами, ParameterTable + FindBiome (поиск с проверкой range-membership + fallback по центру).
|
|
||||||
internal/worldgen/climate_sampler.go — SampleColumn: сэмплирует 6 climate-осей на поверхности (depth зафиксирован = 0 для surface-layer).
|
|
||||||
internal/world/biome_lookup.go — embed таблицы biome_parameters.json (3.5 МБ, 7593 записей → ~3800 surface-записей после фильтра depth=0), парсинг с гибкой обработкой depth (скаляр/массив), BiomeAt(od, wx, wz) → uint16 через registry.Index.
|
|
||||||
internal/world/biome_parameters.json — копия ванильной таблицы параметров.
|
|
||||||
Тесты: worldgen/biome_test.go (квантование, fitDistance, диапазоны, детерминизм, загрузка climate-полей) + world/biome_lookup_test.go (детерминизм, разнообразие биомов в мире, threading биома в чанк).
|
|
||||||
Изменённые файлы
|
|
||||||
internal/worldgen/loader.go — OverworldDensity получил 6 climate-полей (Temperature/Humidity/Continentalness/Erosion/Weirdness/Depth); LoadOverworldFinalDensity теперь парсит router-ключи temperature/vegetation/continents/erosion/ridges/depth через существующий parseNode-механизм.
|
|
||||||
internal/world/vanilla.go — generateVanilla теперь вызывает BiomeAt в центре чанка вместо захардкоженного BiomePlains.
|
|
||||||
Ключевой результат
|
|
||||||
Тест TestBiomeAtVaryingAcrossWorld подтвердил: 5 различных биомов в области 16×16 чанков для сида 12345. Раньше весь мир был plains — теперь видны климатические зоны.
|
|
||||||
|
|
||||||
Декомпозиция решённых подзадач
|
|
||||||
Загрузка climate-узлов из того же noise_router (без новых DF-типов — parseObject уже поддерживает shifted_noise/flat_cache/add).
|
|
||||||
Бит-совместимая с ваниллой климат-математика (квантование + fitDistance).
|
|
||||||
Гибкий парсинг таблицы (нашёл и обработал edge-case: ~2 записи с depth как массив — это пещерные биомы).
|
|
||||||
name→ID через существующий registry.Index (не понадобилось хардкодить ID биомов).
|
|
||||||
Проверка
|
|
||||||
go vet ./... — чисто
|
|
||||||
go test ./... — все тесты зелёные (nbt, world, worldgen), включая golden/encode/parity (encoding не менялся, биом теперь single-valued per-chunk — корректно).
|
|
||||||
Что дальше (этап 2 — per-cell 3D)
|
|
||||||
Инфраструктура полностью готова. Для перехода на 4×4×4 biomes останется:
|
|
||||||
|
|
||||||
vanilla.go — сэмплить climate на сетке 4×4×4 (1536 раз/чанк), real depth (зависит от Y) → underground/cave biomes.
|
|
||||||
encode.go — переписать writeSection biome-контейнер с single-valued на paletted (64 ячейки/секция, min bpe=1).
|
|
||||||
worldgen/biome.go — добавить R-tree/BoundTree индекс для производительности (1536 brute-force поисков/чанк слишком медленно).
|
|
||||||
Затем — surface rules (B2) раскроются полностью (mycelium, терракота, снег).
|
|
||||||
Запустить сервер вручную (regionio -seed 12345), чтобы визуально убедиться в разных биомах?
|
|
||||||
|
|
||||||
|
|
||||||
7 files changed
|
|
||||||
+536 -15
|
|
||||||
да давай
|
|
||||||
Worked for 1m 11s
|
|
||||||
Сервер остановлен.
|
|
||||||
|
|
||||||
End-to-end тест прошёл успешно ✅
|
|
||||||
Игрок подключился реальным клиентом (Fabric, протокол 775) и провёл в мире более 15 секунд без единой ошибки. Из логов:
|
|
||||||
|
|
||||||
Этап Результат
|
|
||||||
Handshake (next=2 login) ✅
|
|
||||||
Login → player logged in (name=Zoydi, offline UUID) ✅
|
|
||||||
Configuration: brand, registry data (28 реестров), known-packs ✅
|
|
||||||
Play phase: entered play phase → player loaded into world ✅
|
|
||||||
Chunk streaming: 81 чанк при спавне, потом +9 при каждом шаге ✅
|
|
||||||
Player movement: center_x/center_z меняется → onPlayerMove работает ✅
|
|
||||||
Keep-alive ack через 15с ✅
|
|
||||||
Критично для нашей задачи: чанки генерируются с динамическими биомами (BiomeAt на каждом чанке), и клиент их принимает без рассинхронизации — значит single-valued biome-container корректно несёт per-chunk биом, и никаких несоответствий с реестром нет.
|
|
||||||
|
|
||||||
Итог двух этапов
|
|
||||||
За эту сессию мы прошли от «мира-заглушки» до узнаваемого ванильного мира:
|
|
||||||
|
|
||||||
Реальный сид (-seed / REGIONIO_SEED) — мир воспроизводим.
|
|
||||||
Фикс поверхности — bedrock-слой, правильные пляжи, гравий на глубокой воде.
|
|
||||||
Multi-noise биомы (этап 1) — climate-sampler + бит-совместимый Climate.findTopBiome → в области 16×16 чанков видны 5 различных биомов вместо сплошного plains.
|
|
||||||
Следующий логический шаг (этап 2) — per-cell 4×4×4 биомы: сэмплить climate 1536 раз/чанк с настоящим depth → underground/cave biomes + paletted biome container. Инфраструктура (climate math, finder, таблица) уже готова, нужно только (а) R-tree для производительности и (б) переписать writeSection на paletted encoding.
|
|
||||||
|
|
||||||
Продолжаем с этапом 2, или сначала закоммитить/завершить текущий прогресс?
|
|
||||||
|
|
||||||
давай закоммитим на Github. https://github.com/Master290/RegionIO у меня установлен gh, уже войденный в аккаунт.
|
|
||||||
Worked for 4m 33s
|
|
||||||
Запушено на GitHub ✅
|
|
||||||
Репозиторий: https://github.com/Master290/RegionIO (ветка main)
|
|
||||||
|
|
||||||
Что попало в коммит (145 файлов)
|
|
||||||
Весь исходный код: cmd/, internal/ (protocol, nbt, registry, world, worldgen, network, server)
|
|
||||||
Embedded-данные: synced_registries.json, synced_tags.bin, biome_parameters.json, item_blocks.json, worldgen datapack
|
|
||||||
Тесты + фикстуры (testdata/vanilla_flat_chunk.bin)
|
|
||||||
go.mod, .gitignore, README.md
|
|
||||||
Что осознанно исключено через .gitignore
|
|
||||||
Исключено Причина
|
|
||||||
server.jar, libraries/, versions/ (123 МБ) Бинарники Mojang, лицензия + размер
|
|
||||||
generated/ (23 МБ, 1506 файлов) Вывод server.jar --reports, регенерируется; не нужен для сборки (все embeds внутри internal/)
|
|
||||||
logs/, .claude/ Логи + личные настройки (в .claude/settings.local.json были пути /home/daniar/...)
|
|
||||||
*.exe, *.jar Билд-артефакты
|
|
||||||
Проверил перед коммитом: server.jar/libraries/versions отсутствуют в staged, бинарных файлов нет.
|
|
||||||
|
|
||||||
Замечание по line-endings
|
|
||||||
Git выдал много предупреждений LF will be replaced by CRLF — это нормальная Windows-нормализация, файлы сохранены с LF в репо. Если хотите зафиксировать поведение, могу добавить .gitattributes (* text=auto eol=lf).
|
|
||||||
|
|
||||||
Репозиторий теперь живой и готов к дальнейшей работе (этап 2 — per-cell биомы, или любая другая задача). Что дальше?
|
|
||||||
|
|
||||||
README.md
|
|
||||||
Document · MD
|
|
||||||
|
|
||||||
2 files changed
|
|
||||||
+94 -0
|
|
||||||
ну да давай второй этап
|
|
||||||
Worked for 3m 6s
|
|
||||||
Запущу параллельно Explore-агентов для точечного исследования: (1) текущий encoder для biome-контейнера и как добавить paletted 4×4×4 формат, (2) как сейчас идёт corner-grid семплинг и куда внедрить 4×4×4 climate sampling, (3) реальные значения depth-полей в таблице параметров для underground biomes.
|
|
||||||
|
|
||||||
SubAgent
|
|
||||||
·
|
|
||||||
Explore biome container encoding
|
|
||||||
SubAgent
|
|
||||||
·
|
|
||||||
Explore climate sampling grid
|
|
||||||
SubAgent
|
|
||||||
·
|
|
||||||
Explore underground biome params
|
|
||||||
|
|
||||||
Thought
|
|
||||||
for a few seconds
|
|
||||||
продолжи
|
|
||||||
Worked for 17m 31s
|
|
||||||
Готово. Этап 2 завершён.
|
|
||||||
|
|
||||||
Итог этапа 2 — per-cell 4×4×4 биомы (3D, с caves)
|
|
||||||
Что сделано
|
|
||||||
Encoding (internal/world/encode.go)
|
|
||||||
|
|
||||||
Chunk.biomes [SectionCount]*[biomeCellsPerSection]uint16 — per-section 3D biome storage (64 cells/section = 4×4×4), nil → fallback на uniform c.biome (flat-мир).
|
|
||||||
SetBiome(lx,y,lz,biome) — лениво аллоцирует секцию, YZX-индекс (by&3)<<4|(bz&3)<<2|(bx&3).
|
|
||||||
writeBiomePalette — аналог writeBlockPalette с biome-специфичными порогами: min bpe=1 (не 4), direct при bpe>bitsFor(totalBiomes=65)=7.
|
|
||||||
buildPalette обобщён под slice (работает и для блоков 4096, и для биомов 64).
|
|
||||||
Climate sampling (internal/worldgen/climate_sampler.go)
|
|
||||||
|
|
||||||
Sample2D struct + SampleColumn2D — 5 осей (temp/humid/cont/ero/weird) сэмплятся раз на колонку (256 раз/chunk), переиспользуются по Y.
|
|
||||||
SampleCell(od, s2D, wx, wy, wz) — переиспользует 5 осей, считает только depth на реальной Y (1 DF-вызов/cell вместо 6).
|
|
||||||
Biome lookup (internal/world/biome_lookup.go)
|
|
||||||
|
|
||||||
depthRange обобщён: скаляр → {v,v+1} band, массив [min,max] → явный range. Позволяет lush/dripstone [0.2,0.9] и deep_dark 1.1.
|
|
||||||
loadBiomeTable грузит все 7593 entries (surface + underground twins + caves) — finder через range-contains по depth выбирает слой автоматически.
|
|
||||||
BiomeAt3D(od, s2D, wx, wy, wz) — per-cell lookup.
|
|
||||||
Generator (internal/world/vanilla.go)
|
|
||||||
|
|
||||||
fillBiomes3D — параллельный цикл (4 горутины): 2D climate раз на колонку, затем 1536 BiomeAt3D вызовов через SetBiome.
|
|
||||||
Результаты
|
|
||||||
Метрика Значение
|
|
||||||
Тесты Все зелёные (go vet чист, go test ./... OK)
|
|
||||||
Cave biomes ✅ lush/dripstone/deep_dark резолвятся корректно (TestCaveBiomesPresent)
|
|
||||||
Flat-мир regression ✅ single-valued container, golden test зелёный
|
|
||||||
3D-биомы overhead < 0.3мс/chunk (58ms total vs 57.8ms baseline) — gate пройден
|
|
||||||
BenchmarkBiomeAt3D 108мкс/вызов × 1536 ≈ 166мс raw, но параллелится и кэшируется в Cache.Frame
|
|
||||||
Server status-ping ✅ отвечает валидным JSON
|
|
||||||
Benchmark note
|
|
||||||
BenchmarkCacheColdJoin = 35ms (cold-gen 81 чанка при спавне) — это узкое место стриминга, но оно доминируется terrain-gen, а не биомами. Это существовало и до этапа 2; оптимизация (если понадобится) — отдельная задача (pre-generated cache, async streaming).
|
|
||||||
|
|
||||||
Закоммитить этап 2 на GitHub?
|
|
||||||
|
|
||||||
|
|
||||||
8 files changed
|
|
||||||
+508 -90
|
|
||||||
да
|
|
||||||
Worked for 46s
|
|
||||||
Запушено ✅
|
|
||||||
Коммит d3142e7 на https://github.com/Master290/RegionIO (ветка main)
|
|
||||||
|
|
||||||
a7bb949..d3142e7 — 7 файлов, +431/−75 строк
|
|
||||||
Remote HEAD подтверждён: 3D per-cell biomes (4x4x4) with surface/underground/cave layers
|
|
||||||
История репозитория
|
|
||||||
|
|
||||||
text
|
|
||||||
d3142e7 3D per-cell biomes (4x4x4) with surface/underground/cave layers ← этап 2
|
|
||||||
a7bb949 Initial commit: RegionIO Minecraft server core ← этап 1 + seed/поверхность
|
|
||||||
Краткая сводка прогресса за сессию
|
|
||||||
Этап Результат
|
|
||||||
Анализ Полный разбор архитектуры и roadmap
|
|
||||||
Seed + поверхность Воспроизводимые миры, bedrock/пляжи/гравий
|
|
||||||
3D-биомы (эт.1) Per-chunk multi-noise, 5 биомов в 16×16
|
|
||||||
3D-биомы (эт.2) Per-cell 4×4×4 + caves (lush/dripstone/deep_dark), <0.3мс overhead
|
|
||||||
Мир RegionIO теперь имеет настоящие ванильные 3D-биомы с пещерными биомами в глубине — всё бит-совместимое с vanilla Climate.findTopBiome.
|
|
||||||
|
|
||||||
Естественные следующие шаги (на выбор)
|
|
||||||
Surface rules (B2) — теперь, когда биомы 3D, раскрываются полностью: mycelium в mushroom_fields, терракота в badlands, снег на высотах, sand в desert.
|
|
||||||
Производительность cold-streaming — BenchmarkCacheColdJoin = 35мс (81 чанк при спавне); async/pre-generation.
|
|
||||||
Сохранение в .mca — мир переживает рестарт (название RegionIO намекает).
|
|
||||||
Структуры — деревья уже есть, дальше деревни/крепости.
|
|
||||||
Что дальше?
|
|
||||||
|
|
||||||
Ну давай по очереди, что считаешь нужным
|
|
||||||
Worked for 3h 1m 6s
|
|
||||||
Лучше напишу standalone тест, который применяет rule напрямую к синтетическому context на Y=70 и печатает результат. Это изолирует проблему от генератора.
|
|
||||||
|
|
||||||
Wrote
|
|
||||||
|
|
||||||
surface_debug_test.go
|
|
||||||
internal/worldgen/
|
|
||||||
+
|
|
||||||
3
|
|
||||||
8
|
|
||||||
|
|
||||||
10 files changed
|
|
||||||
+1195 -199
|
|
||||||
продолжи
|
|
||||||
Worked for 7m 23s
|
|
||||||
Этап B2 завершён и запушен ✅
|
|
||||||
Коммит 4dcf938 на https://github.com/Master290/RegionIO
|
|
||||||
|
|
||||||
Что сделано
|
|
||||||
Полный интерпретатор ванильного surface_rule из overworld.json (2157 строк правил): 4 типа правил (block/sequence/condition/bandlands) + все 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 для всех surface-блоков (grass/sand/terracotta/mycelium/podzol/coarse_dirt/sandstone/calcite/snow/ice + snowy-варианты).
|
|
||||||
Интеграция в генератор: climate + biome сэмплятся до fill-column, rule-tree применяется top-down с stone-дефолтом для глубинных блоков.
|
|
||||||
Ключевые баги, найденные и исправленные при отладке
|
|
||||||
rand.New на каждый блок → 98K аллокаций/chunk → поднят до 109ms. Фикс: один per-column RNG → 65ms.
|
|
||||||
Блоки ниже поверхности возвращали ok=false → становились air → chunk полый. Фикс: дефолт stone.
|
|
||||||
above_preliminary_surface со строгим > блокировал верхний solid-блок → biome-диспетчер не достигался (везде stone). Фикс: >= → появилась трава (9).
|
|
||||||
Доказательство работы
|
|
||||||
TestSurfaceVariesByBiome: до фикса — только stone (1); после — grass (9) ×13 + leaves (279) от деревьев. Изолированный тест: plains→grass, desert→sandstone, badlands→red_sandstone, mushroom→mycelium.
|
|
||||||
|
|
||||||
Производительность
|
|
||||||
71ms/chunk (+13ms vs 58ms baseline) — в рамках gate, кэшируется в Cache.Frame.
|
|
||||||
|
|
||||||
История репозитория
|
|
||||||
|
|
||||||
text
|
|
||||||
4dcf938 Biome-aware surface rules from the vanilla rule tree ← B2
|
|
||||||
d3142e7 3D per-cell biomes (4x4x4) with surface/underground/cave layers
|
|
||||||
a7bb949 Initial commit: RegionIO Minecraft server core
|
|
||||||
Честная оговорка
|
|
||||||
Я подтвердил корректность surface rules unit-тестами (biome→блок) и реальной генерацией (grass появился), но не делал визуальную проверку в клиенте в этом этапе. Семантика некоторых условий (stone_depth ceiling, bandlands геометрия, noise_threshold с реальным surface-шумом) основана на моём понимании ваниллы и может требовать сверки с server.jar для битового паритета. Если при подключении поверхность выглядит не так — это первое место для проверки.
|
|
||||||
|
|
||||||
Естественные следующие шаги: сохранение в .mca (название RegionIO намекает), производительность cold-streaming (35ms/81-chunk при спавне), или структуры (деревья уже есть, дальше деревни). Что дальше?
|
|
||||||
|
|
||||||
|
|
||||||
3 files changed
|
|
||||||
+55 -46
|
|
||||||
давай дальше
|
|
||||||
Worked for 17m 30s
|
|
||||||
Все 7 persistence-тестов прошли, включая end-to-end «restart survives» и autosave. Полный набор + vet.
|
|
||||||
|
|
||||||
Explore
|
|
||||||
·
|
|
||||||
1 file
|
|
||||||
|
|
||||||
10 files changed
|
|
||||||
+1378 -29
|
|
||||||
продолжи
|
|
||||||
Worked for 1m 20s
|
|
||||||
Персистенция мира в .mca завершена и запушена ✅
|
|
||||||
Коммит d1cc29b на https://github.com/Master290/RegionIO — RegionIO наконец-то делает region I/O.
|
|
||||||
|
|
||||||
Что сделано
|
|
||||||
regionfile.go — Anvil .mca контейнер: 8192-байтный заголовок (offset + timestamp таблицы), 4096-байтные сектора, zlib-компрессия chunk-записей (compression type 2). Правильная floor-division для negative chunk coords (chunk -1 → region -1, local 31).
|
|
||||||
store.go — chunk ↔ Level-nested NBT: per-section block_states/biomes palettes (именованные, не int-ID), WORLD_SURFACE heightmap, DataVersion=4790 (извлечён из server.jar), yPos=-4. Store кэширует по одному RegionFile на регион.
|
|
||||||
state_names.go — мост int-ID ↔ named-palette через embedded blocks.json (полный, ~30K состояний, fallback air).
|
|
||||||
compress.go — zlib-обёртки.
|
|
||||||
cache.go — read-through (диск → генерация), dirty-tracking, StartAutosave (возвращает done-channel чтобы saver завершился до Close), SaveAll.
|
|
||||||
server.go + main.go — Config.WorldDir (по умолчанию "world"), -world флаг, autosave каждые 30с, SaveAll + Store.Close при SIGINT/SIGTERM.
|
|
||||||
Ключевые баги, найденные и исправленные
|
|
||||||
Windows file-handle leak — RegionFile handles не закрывались → t.TempDir() не мог удалить .mca. Фикс: store.Close() в тестах.
|
|
||||||
Autosave/Close race — Close() занулял regions map, а фоновый saver ещё писал → assignment to entry in nil map. Фикс: StartAutosave возвращает done-channel, caller ждёт его перед Close.
|
|
||||||
Доказательства работы (7 тестов, все зелёные)
|
|
||||||
Тест Что проверяет
|
|
||||||
TestRegionFileRoundTrip zlib-цикл сохраняет payload
|
|
||||||
TestRegionFileAbsentChunk offset=0 → ErrChunkNotFound
|
|
||||||
TestRegionFileOverwrite повторная запись заменяет данные
|
|
||||||
TestStoreChunkRoundTrip NBT-кодирование блоков/биомов
|
|
||||||
TestStoreSaveLoadIntegration end-to-end «рестарт»: edit → SaveAll → новый Cache → edit виден
|
|
||||||
TestStoreNegativeCoords chunk -1 → r.-1.-1.mca
|
|
||||||
TestCacheAutosavePersistsEdits фоновый saver сохраняет правки без SaveAll
|
|
||||||
История репозитория
|
|
||||||
|
|
||||||
text
|
|
||||||
d1cc29b Chunk persistence to Anvil .mca region files ← .mca
|
|
||||||
4dcf938 Biome-aware surface rules from the vanilla rule tree
|
|
||||||
d3142e7 3D per-cell biomes (4x4x4) with surface/underground/cave layers
|
|
||||||
a7bb949 Initial commit: RegionIO Minecraft server core
|
|
||||||
Честная оговорка
|
|
||||||
Тесты доказывают, что наша запись/чтение самосогласованы (round-trip работает, restart сохраняет правки). Но я не проверял совместимость с реальным ванильным клиентом/сервером — то есть что сторонние тулзы (MCEdit, vanilla client) прочитают наши .mca. Возможные несовпадения: structures заглушка (vanilla может требовать references), Status:"full" без фактического post-processing, пропущенные heightmaps. Если ванилла не откроет — это первое место для проверки.
|
|
||||||
|
|
||||||
Естественные следующие шаги
|
|
||||||
Производительность cold-streaming — BenchmarkCacheColdJoin = 35мс (81 чанк при спавне); теперь можно pre-generate + async.
|
|
||||||
Эвикция чанков из памяти — map растёт без границ при стриминге.
|
|
||||||
Структуры — деревья есть, дальше деревни/крепости.
|
|
||||||
Что дальше?
|
|
||||||
|
|
||||||
ну давай наверное cold streaming
|
|
||||||
Worked for 13m 39s
|
|
||||||
Сервер отвечает валидным status-JSON. Streamer подключён и не падает при boot. Остановлю сервер и закоммичу.
|
|
||||||
|
|
||||||
Ran
|
|
||||||
cd "C:\Users\Daniar\Downloads\regionio" && taskkill //F //IM regionio.exe 2>&1 | head -1; rm -rf /tmp/regionio-stream-test
|
|
||||||
Ran
|
|
||||||
cd "C:\Users\Daniar\Downloads\regionio" && git add -A && git status --short 2>&1 | head; echo "=== commit ==="; git -c user.name="Master290" -c user.email="Master290@users.noreply.github.com" commit -m "Background predictive chunk streaming 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." 2>&1 | tail -4
|
|
||||||
|
|
||||||
6 files changed
|
|
||||||
+551 -77
|
|
||||||
продолжи
|
|
||||||
Worked for 49s
|
|
||||||
Background predictive streaming завершён и запушен ✅
|
|
||||||
Коммит d7c80a8 на https://github.com/Master290/RegionIO
|
|
||||||
|
|
||||||
Что сделано
|
|
||||||
network/streamer.go — per-connection background streamer:
|
|
||||||
requestRecenter — non-blocking push (read-loop мгновенно свободен).
|
|
||||||
spiralOrder — чанки от центра наружу (ближайшие к игроку первыми).
|
|
||||||
parallelSend — пул воркеров (min(NumCPU, 8)) генерят Cache.Frame параллельно (Cache уже потокобезопасен), frames отправляются серийно под write-mutex.
|
|
||||||
parallelGenerate — predictive ring: предгенерирует кольцо viewRadius+1 так, что при движении чанки уже готовы в кэше (без pop-in).
|
|
||||||
network/play.go — beginPlay запускает streamer вместо блокирующего streamAround; onPlayerMove → requestRecenter (non-blocking).
|
|
||||||
network/handler.go — ctx (connection lifetime) + streamer поле; streamer останавливается при выходе из read-loop (cancel на serve exit).
|
|
||||||
network/configuration.go — client view_distance сохраняется (clamp 2..16) и задаёт радиус streamer.
|
|
||||||
До / после
|
|
||||||
Метрика До (блокирующий) После (background)
|
|
||||||
Read-loop при спавне Заморожен ~5.7с (81 чанк серийно @ 71мс) Мгновенно свободен
|
|
||||||
Порядок прибытия чанков Произвольный (строчный) От центра наружу (спираль)
|
|
||||||
Параллелизм генерации Серийно (1) Пул воркеров (до 8×)
|
|
||||||
Движение игрока Pop-in (чанки не готовы) Predictive ring (готовы заранее)
|
|
||||||
View distance Захардкожен 4 Из client_information
|
|
||||||
История репозитория
|
|
||||||
|
|
||||||
text
|
|
||||||
d7c80a8 Background predictive chunk streaming ← streaming
|
|
||||||
d1cc29b Chunk persistence to Anvil .mca region files
|
|
||||||
4dcf938 Biome-aware surface rules from the vanilla rule tree
|
|
||||||
d3142e7 3D per-cell biomes (4x4x4) with surface/underground/cave layers
|
|
||||||
a7bb949 Initial commit: RegionIO Minecraft server core
|
|
||||||
Честная оговорка
|
|
||||||
Я подтвердил unit-тестами (spiral order, non-blocking recenter) и что сервер загружается и отвечает на status-ping. Полноценную визуальную проверку плавности стриминга в клиенте я не делал — стоит подключиться реальным клиентом и убедиться, что: (1) мир появляется плавно без заморозки при спавне, (2) при быстром беге нет pop-in (predictive ring работает), (3) нет утечки горутин (disconnect/reconnect). Если что-то выглядит не так — race между block-edit и streamer (last-write-wins) или дублирующая отправка — это первые места для проверки.
|
|
||||||
|
|
||||||
Естественные следующие шаги
|
|
||||||
Эвикция чанков из памяти — Cache.chunks растёт без границ; predictive streamer усугубляет (предгенерит больше, чем нужно).
|
|
||||||
Структуры — деревья есть, дальше деревни/крепости.
|
|
||||||
Mob spawning / боевая система — для «играбельности».
|
|
||||||
Что дальше?
|
|
||||||
|
|
||||||
давай эвикцию
|
|
||||||
Worked for 15m 38s
|
|
||||||
LRU chunk eviction завершён и запушен ✅
|
|
||||||
Коммит 65a7445 на https://github.com/Master290/RegionIO
|
|
||||||
|
|
||||||
Что сделано
|
|
||||||
world/cache.go — LRU-политика через container/list + index-map (O(1) на операцию):
|
|
||||||
touch(key) — move-to-front на каждом chunkAt/Frame/SetBlock hit.
|
|
||||||
evictIfNeeded() — при превышении лимита выкидывает LRU (back of list), дропая оба chunks[key] + frames[key].
|
|
||||||
Dirty-safe: dirty-чанки не эвицируются (бампятся в MRU, ждут autosave flush) — правки игрока никогда не теряются.
|
|
||||||
NewCacheWithLimit(threshold, gen, store, maxChunks) — полный конструктор; maxChunks=0 → unbounded (backward-compat).
|
|
||||||
server/server.go — Config.MaxCachedChunks (по умолчанию 1024 ≈ 200МБ), wired в New при наличии WorldDir.
|
|
||||||
cmd/regionio/main.go — -maxcache флаг.
|
|
||||||
Доказательства работы (6 тестов, все зелёные)
|
|
||||||
Тест Что проверяет
|
|
||||||
TestEvictionRespectsLimit кэш ≤ 4 после вставки 6, старейшие эвиктированы
|
|
||||||
TestEvictionLRUOrder touched-чанк (A) выживает, нетронутый (B) эвиктирован
|
|
||||||
TestEvictionDropsBothMaps дроп и chunks, и frames (нет утечки)
|
|
||||||
TestEvictionKeepsDirty dirty-чанк НЕ эвиктирован (правки сохранены)
|
|
||||||
TestEvictionReloadsOnAccess эвиктированный чанк регенерируется по запросу
|
|
||||||
TestEvictionReloadPreservesEdits end-to-end: edit → SaveAll → evict → reload с диска → правка на месте
|
|
||||||
История репозитория
|
|
||||||
|
|
||||||
text
|
|
||||||
65a7445 LRU chunk eviction (bounded cache, default 1024 chunks / ~200MB) ← eviction
|
|
||||||
d7c80a8 Background predictive chunk streaming
|
|
||||||
d1cc29b Chunk persistence to Anvil .mca region files
|
|
||||||
4dcf938 Biome-aware surface rules from the vanilla rule tree
|
|
||||||
d3142e7 3D per-cell biomes (4x4x4) with surface/underground/cave layers
|
|
||||||
a7bb949 Initial commit: RegionIO Minecraft server core
|
|
||||||
Итог по инфраструктуре мира
|
|
||||||
За последние этапы мир RegionIO получил полный production-стек:
|
|
||||||
|
|
||||||
3D-биомы (surface + caves) — бит-совместимо с vanilla Climate
|
|
||||||
Surface rules — биом-aware поверхность из ванильного rule-tree
|
|
||||||
.mca персистенция — мир переживает рестарт, autosave + save-on-shutdown
|
|
||||||
Background streaming — read-loop не блокируется, predictive pre-gen
|
|
||||||
LRU eviction — память предсказуема (~200МБ), без утечек при exploration
|
|
||||||
Сервер теперь может работать долго с одним/несколькими игроками без OOM и без потери прогресса.
|
|
||||||
|
|
||||||
Естественные следующие шаги
|
|
||||||
Структуры — деревья есть (decorate), дальше деревни/крепости/mineshafts.
|
|
||||||
Mob spawning — для «играбельности» (HP, AI, дроп, боевая система).
|
|
||||||
Полноценное освещение — сейчас full-bright stub; тёмные пещеры, мобы от света.
|
|
||||||
Nether/End измерения — multi-dimension support.
|
|
||||||
Что дальше?
|
|
||||||
|
|
||||||
|
|
||||||
4 files changed
|
|
||||||
+272 -11
|
|
||||||
Давай структуры
|
|
||||||
Worked for 7m 53s
|
|
||||||
Метода nextFloat() нет — добавлю его в vanilla.go (рядом с next).
|
|
||||||
|
|
||||||
Explore
|
|
||||||
·
|
|
||||||
1 file
|
|
||||||
|
|
||||||
1 file changed
|
|
||||||
+223 -0
|
|
||||||
|
|
||||||
features.go
|
|
||||||
internal/world/
|
|
||||||
+223
|
|
||||||
|
|
@ -23,6 +23,21 @@ import (
|
||||||
// from versions/.../server.jar's version.json "world_version".
|
// from versions/.../server.jar's version.json "world_version".
|
||||||
const dataVersion26 = 4790
|
const dataVersion26 = 4790
|
||||||
|
|
||||||
|
// generatorVersion identifies the output of the current chunk generator. Every
|
||||||
|
// chunk we save carries it, and loading rejects any chunk stamped differently.
|
||||||
|
//
|
||||||
|
// BUMP THIS in any commit that changes what the generator produces.
|
||||||
|
//
|
||||||
|
// Without it a world directory silently pins whatever the generator did the
|
||||||
|
// first time it ran: chunkAt prefers the store over the generator, so the
|
||||||
|
// already-explored area around spawn keeps its old terrain and every later fix
|
||||||
|
// looks like it did nothing in exactly the place you are standing.
|
||||||
|
const generatorVersion = 1
|
||||||
|
|
||||||
|
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||||
|
// because it is ours, not part of the vanilla chunk format.
|
||||||
|
const generatorVersionTag = "RegionIOGeneratorVersion"
|
||||||
|
|
||||||
// minYSection is the on-disk "yPos": the section index at MinY (-64 → -4),
|
// minYSection is the on-disk "yPos": the section index at MinY (-64 → -4),
|
||||||
// since sections are 16 blocks tall and the overworld is 24 sections from
|
// since sections are 16 blocks tall and the overworld is 24 sections from
|
||||||
// section index -4 to 19.
|
// section index -4 to 19.
|
||||||
|
|
@ -254,6 +269,7 @@ func chunkToNBT(c *Chunk) *nbt.Compound {
|
||||||
|
|
||||||
return nbt.NewCompound().
|
return nbt.NewCompound().
|
||||||
Set("DataVersion", nbt.Int(dataVersion26)).
|
Set("DataVersion", nbt.Int(dataVersion26)).
|
||||||
|
Set(generatorVersionTag, nbt.Int(generatorVersion)).
|
||||||
Set("Level", level)
|
Set("Level", level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -399,6 +415,16 @@ func packIndices(ids []uint16, indexOf map[uint16]int) nbt.LongArray {
|
||||||
// absolute coordinates are derived from the on-disk xPos/zPos (authoritative);
|
// absolute coordinates are derived from the on-disk xPos/zPos (authoritative);
|
||||||
// the region/local coords passed in are used only to validate.
|
// the region/local coords passed in are used only to validate.
|
||||||
func nbtToChunk(root *nbt.Compound, regionX, regionZ, localX, localZ int) (*Chunk, error) {
|
func nbtToChunk(root *nbt.Compound, regionX, regionZ, localX, localZ int) (*Chunk, error) {
|
||||||
|
// Reject anything the current generator did not produce so the caller
|
||||||
|
// regenerates instead of serving stale terrain. Chunks written before the
|
||||||
|
// stamp existed have no tag and decode as 0, so they are invalidated too.
|
||||||
|
// This is per-chunk on purpose: the world metadata file guards the seed,
|
||||||
|
// which is a hard mismatch, while a generator change is routine and should
|
||||||
|
// quietly regenerate rather than refuse to open the world.
|
||||||
|
if v := nbtAsInt(root, generatorVersionTag); v != generatorVersion {
|
||||||
|
return nil, ErrChunkNotFound
|
||||||
|
}
|
||||||
|
|
||||||
levelTag, ok := root.Get("Level")
|
levelTag, ok := root.Get("Level")
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("world: chunk NBT missing Level")
|
return nil, fmt.Errorf("world: chunk NBT missing Level")
|
||||||
|
|
|
||||||
|
|
@ -495,3 +495,56 @@ func TestConcurrentAutosavePreservesLatestEdit(t *testing.T) {
|
||||||
t.Fatalf("persisted final block = %d, want %d", got, StateBedrock)
|
t.Fatalf("persisted final block = %d, want %d", got, StateBedrock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGeneratorVersionStampRejectsStaleChunks covers the guard that keeps a
|
||||||
|
// saved world from masking generator changes.
|
||||||
|
//
|
||||||
|
// chunkAt prefers the store over the generator, so without this check a chunk
|
||||||
|
// saved by an older build is served forever and every later worldgen fix looks
|
||||||
|
// like it did nothing in the already-explored area around spawn. Chunks written
|
||||||
|
// before the stamp existed carry no tag and must be rejected the same way.
|
||||||
|
func TestGeneratorVersionStampRejectsStaleChunks(t *testing.T) {
|
||||||
|
toRoot := func(t *testing.T, c *nbt.Compound) *nbt.Compound {
|
||||||
|
t.Helper()
|
||||||
|
_, tag, err := nbt.UnmarshalNamed(nbt.MarshalNamed("", c))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
root, ok := tag.(*nbt.Compound)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("root is not a compound")
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("current version loads", func(t *testing.T) {
|
||||||
|
root := toRoot(t, chunkToNBT(NewChunk(0, 0, BiomePlains)))
|
||||||
|
if _, err := nbtToChunk(root, 0, 0, 0, 0); err != nil {
|
||||||
|
t.Fatalf("nbtToChunk on a freshly written chunk: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("older version is rejected", func(t *testing.T) {
|
||||||
|
c := chunkToNBT(NewChunk(0, 0, BiomePlains))
|
||||||
|
c.Set(generatorVersionTag, nbt.Int(generatorVersion-1))
|
||||||
|
if _, err := nbtToChunk(toRoot(t, c), 0, 0, 0, 0); err != ErrChunkNotFound {
|
||||||
|
t.Errorf("err = %v, want ErrChunkNotFound so the chunk regenerates", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing stamp is rejected", func(t *testing.T) {
|
||||||
|
// What every chunk written before this guard existed looks like.
|
||||||
|
c := chunkToNBT(NewChunk(0, 0, BiomePlains))
|
||||||
|
stripped := nbt.NewCompound()
|
||||||
|
for _, name := range c.Keys() {
|
||||||
|
if name == generatorVersionTag {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v, _ := c.Get(name)
|
||||||
|
stripped.Set(name, v)
|
||||||
|
}
|
||||||
|
if _, err := nbtToChunk(toRoot(t, stripped), 0, 0, 0, 0); err != ErrChunkNotFound {
|
||||||
|
t.Errorf("err = %v, want ErrChunkNotFound so the chunk regenerates", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue