diff --git a/.gitignore b/.gitignore index ceb53e7..67d07b6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,22 @@ # All build-time embeds live under internal/; generated/ is not needed to build. /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/ *.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 /.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33392ae --- /dev/null +++ b/CLAUDE.md @@ -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`. diff --git a/chat.md b/chat.md deleted file mode 100644 index 6f84ce4..0000000 --- a/chat.md +++ /dev/null @@ -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: (например 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 diff --git a/internal/world/store.go b/internal/world/store.go index d6908a7..30b0b9f 100644 --- a/internal/world/store.go +++ b/internal/world/store.go @@ -23,6 +23,21 @@ import ( // from versions/.../server.jar's version.json "world_version". 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), // since sections are 16 blocks tall and the overworld is 24 sections from // section index -4 to 19. @@ -254,6 +269,7 @@ func chunkToNBT(c *Chunk) *nbt.Compound { return nbt.NewCompound(). Set("DataVersion", nbt.Int(dataVersion26)). + Set(generatorVersionTag, nbt.Int(generatorVersion)). 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); // the region/local coords passed in are used only to validate. 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") if !ok { return nil, fmt.Errorf("world: chunk NBT missing Level") diff --git a/internal/world/store_test.go b/internal/world/store_test.go index 4b1abbd..1731d03 100644 --- a/internal/world/store_test.go +++ b/internal/world/store_test.go @@ -495,3 +495,56 @@ func TestConcurrentAutosavePreservesLatestEdit(t *testing.T) { 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) + } + }) +} diff --git a/world/region/r.-1.-1.mca b/world/region/r.-1.-1.mca deleted file mode 100644 index 9df4422..0000000 Binary files a/world/region/r.-1.-1.mca and /dev/null differ diff --git a/world/region/r.-1.0.mca b/world/region/r.-1.0.mca deleted file mode 100644 index 85a95cf..0000000 Binary files a/world/region/r.-1.0.mca and /dev/null differ diff --git a/world/region/r.0.-1.mca b/world/region/r.0.-1.mca deleted file mode 100644 index 879bb09..0000000 Binary files a/world/region/r.0.-1.mca and /dev/null differ diff --git a/world/region/r.0.0.mca b/world/region/r.0.0.mca deleted file mode 100644 index dae2152..0000000 Binary files a/world/region/r.0.0.mca and /dev/null differ