feat/worldgen-versioning #3

Merged
Serkyo merged 6 commits from feat/worldgen-versioning into dev 2026-09-11 23:52:54 +00:00
Owner

What does this PR do?

Replaces the single worldgen_version field in level.dat with an append-only worldgen version history, and makes the server honour a chunk's stamped version on both load and write-back.

  • shared::save::level: WorldMetadata now carries a WorldgenHistory { current, versions }, where each WorldgenVersionEntry records a kernel discriminant (PERLIN_KERNEL, the only structural generator today) and the WorldGenConfig that version generated at. A chunk's stored version is an index into versions, so the parameters needed to reproduce any pinned chunk's baseline travel with the world rather than with the shipped asset directory. Accessors are worldgen_current(), worldgen_entry(version) and worldgen_versions().
  • WORLD_FORMAT_VERSION goes to 2. decode() still reads a version 1 record and migrates it through WorldMetadataV1::migrate, backfilling worldgen_version + 1 entries from the caller-supplied pre-history config. It takes a legacy_config argument for that reason.
  • crates/server/src/main.rs builds a BTreeMap<u32, VoxelGenerator> registry from the loaded history, one generator per version, all seeded from the world seed. ServerWorld holds the registry behind an Arc alongside the current generator.
  • Resident chunks are wrapped in ResidentChunk { chunk, worldgen_version }. load_chunk regenerates a saved diff's baseline with the generator for data.worldgen_version(), and reconcile stamps the eviction diff with the version the chunk actually holds instead of a hard-coded 0. A stamp that names no registry entry falls back to the current generator with a warn!, since on-disk history is not proof against a hand-edited save.
  • RegionFile::open and SaveActor::spawn take current_version, so a freshly created region pins its base_worldgen_version to the world's current version rather than 0.
  • assets/data/worldgen/default.json moves to assets/data/worldgen/default/0.json. That file is now version 0's frozen input, read only when level.dat does not exist and a first history entry has to be minted.

Why is this change necessary?

ADR-0009 stores modified chunks as a sparse diff against the deterministic worldgen baseline, and ChunkData already carried a worldgen_version to say which baseline a diff was pinned to. Nothing honoured it. load_chunk regenerated every baseline at the current generator and reconcile wrote every diff back stamped 0, with TODOs in both places saying so. docs/save_format.md records the same gap.

That was harmless only while one version could ever exist. The moment terrain tuning changes, an old chunk's edits would be layered over terrain that never produced them, and its write-back would relabel the diff as belonging to the new baseline, so the original mapping is lost on disk with no way to recover it.

The history has to live in level.dat rather than in assets/, because a version's config must remain readable for the lifetime of any chunk pinned to it. Keeping the configs in the asset directory would mean a shipped-asset edit silently reinterprets every existing save, and it would break worlds carried between builds.

Scope of Changes

server, shared, assets

Testing

cargo test -p shared -p server: 207 passing in shared, 47 in server, no failures. cargo clippy --all-targets --all-features -- -D warnings and cargo fmt --all -- --check are both clean.

New coverage:

  • shared::tests::level::migrates_a_legacy_record_into_a_history builds a version 1 record on disk via a test-local encode_v1 that mirrors the old framing, decodes it, and asserts the resulting history has one entry per legacy version with current preserved.
  • Existing level round-trip tests were extended to cover the history payload, with sample_config() giving every WorldGenConfig field a distinct value so a field swapped during (de)serialization shows up.
  • server::tests::main::shipped_worldgen_asset_matches_its_golden_digest pins an FNV-1a digest over a fixed chunk sample generated from assets/data/worldgen/default/0.json. It catches an edit to the shipped tuning, which is a different failure from the generator-code digest already in shared::generator::tests. Re-pinning it is correct when version 0 is retuned before release, and is a defect signal otherwise.
  • region_file and world_server tests were updated for the new current_version parameters, including a region opened at a non-zero current version.

Not covered by automated tests: there is no second worldgen version to reach yet, so the cross-version load and write-back path is exercised only through hand-constructed stamps. Full verification waits on the first real version bump.

Additional Context

Backwards compatibility. A version 1 level.dat loads and is migrated in place. The migration assumes every pre-history version was generated by the one shipped config, which holds because per-version configs did not exist before this branch. No released build writes version 1, so the legacy path exists for local worlds only and can be dropped later.

Known limitation, tracked in code. ChunkCache is keyed by position alone, not by (position, version). Correct while a single version generates any given position's baseline. Once a second version is reachable, a baseline cached under one version's generation would be handed back for another. There is a NOTE at the eviction site in world_server.rs; the fix belongs with the version-bump work that first makes a second version reachable.

Registry keying. The registry is keyed by version alone. Dimension keying would need a second dimension, which does not exist in the codebase yet. BTreeMap over HashMap for the determinism rule in AGENTS.md.

Type changes worth a reviewer's eye. WorldMetadata loses Copy and Eq because it now owns a Vec. WorldGenConfig gains PartialEq and VoxelGenerator gains Clone, the latter so main.rs can hand ServerWorld the current generator without a second construction.

Scripting layer. No gameplay logic is involved, so nothing here needs a Lua binding. This is save-format and server-internal plumbing.

No response

Checklist

  • I have branched from dev (or a feature branch off dev) and my PR targets dev.
  • I have kept my changes focused to a single concept.
  • I have added or updated documentation (/// doc comments for Rust) where necessary.
  • I have tested my changes and described any relevant automated or manual testing above.
### What does this PR do? Replaces the single `worldgen_version` field in `level.dat` with an append-only worldgen version history, and makes the server honour a chunk's stamped version on both load and write-back. - `shared::save::level`: `WorldMetadata` now carries a `WorldgenHistory { current, versions }`, where each `WorldgenVersionEntry` records a `kernel` discriminant (`PERLIN_KERNEL`, the only structural generator today) and the `WorldGenConfig` that version generated at. A chunk's stored version is an index into `versions`, so the parameters needed to reproduce any pinned chunk's baseline travel with the world rather than with the shipped asset directory. Accessors are `worldgen_current()`, `worldgen_entry(version)` and `worldgen_versions()`. - `WORLD_FORMAT_VERSION` goes to `2`. `decode()` still reads a version `1` record and migrates it through `WorldMetadataV1::migrate`, backfilling `worldgen_version + 1` entries from the caller-supplied pre-history config. It takes a `legacy_config` argument for that reason. - `crates/server/src/main.rs` builds a `BTreeMap<u32, VoxelGenerator>` registry from the loaded history, one generator per version, all seeded from the world seed. `ServerWorld` holds the registry behind an `Arc` alongside the current generator. - Resident chunks are wrapped in `ResidentChunk { chunk, worldgen_version }`. `load_chunk` regenerates a saved diff's baseline with the generator for `data.worldgen_version()`, and `reconcile` stamps the eviction diff with the version the chunk actually holds instead of a hard-coded `0`. A stamp that names no registry entry falls back to the current generator with a `warn!`, since on-disk history is not proof against a hand-edited save. - `RegionFile::open` and `SaveActor::spawn` take `current_version`, so a freshly created region pins its `base_worldgen_version` to the world's current version rather than `0`. - `assets/data/worldgen/default.json` moves to `assets/data/worldgen/default/0.json`. That file is now version 0's frozen input, read only when `level.dat` does not exist and a first history entry has to be minted. ### Why is this change necessary? ADR-0009 stores modified chunks as a sparse diff against the deterministic worldgen baseline, and `ChunkData` already carried a `worldgen_version` to say which baseline a diff was pinned to. Nothing honoured it. `load_chunk` regenerated every baseline at the current generator and `reconcile` wrote every diff back stamped `0`, with `TODO`s in both places saying so. `docs/save_format.md` records the same gap. That was harmless only while one version could ever exist. The moment terrain tuning changes, an old chunk's edits would be layered over terrain that never produced them, and its write-back would relabel the diff as belonging to the new baseline, so the original mapping is lost on disk with no way to recover it. The history has to live in `level.dat` rather than in `assets/`, because a version's config must remain readable for the lifetime of any chunk pinned to it. Keeping the configs in the asset directory would mean a shipped-asset edit silently reinterprets every existing save, and it would break worlds carried between builds. ### Scope of Changes server, shared, assets ### Testing `cargo test -p shared -p server`: 207 passing in `shared`, 47 in `server`, no failures. `cargo clippy --all-targets --all-features -- -D warnings` and `cargo fmt --all -- --check` are both clean. New coverage: - `shared::tests::level::migrates_a_legacy_record_into_a_history` builds a version `1` record on disk via a test-local `encode_v1` that mirrors the old framing, decodes it, and asserts the resulting history has one entry per legacy version with `current` preserved. - Existing `level` round-trip tests were extended to cover the history payload, with `sample_config()` giving every `WorldGenConfig` field a distinct value so a field swapped during (de)serialization shows up. - `server::tests::main::shipped_worldgen_asset_matches_its_golden_digest` pins an FNV-1a digest over a fixed chunk sample generated from `assets/data/worldgen/default/0.json`. It catches an edit to the shipped tuning, which is a different failure from the generator-code digest already in `shared::generator::tests`. Re-pinning it is correct when version 0 is retuned before release, and is a defect signal otherwise. - `region_file` and `world_server` tests were updated for the new `current_version` parameters, including a region opened at a non-zero current version. Not covered by automated tests: there is no second worldgen version to reach yet, so the cross-version load and write-back path is exercised only through hand-constructed stamps. Full verification waits on the first real version bump. ### Additional Context **Backwards compatibility.** A version 1 `level.dat` loads and is migrated in place. The migration assumes every pre-history version was generated by the one shipped config, which holds because per-version configs did not exist before this branch. No released build writes version 1, so the legacy path exists for local worlds only and can be dropped later. **Known limitation, tracked in code.** `ChunkCache` is keyed by position alone, not by `(position, version)`. Correct while a single version generates any given position's baseline. Once a second version is reachable, a baseline cached under one version's generation would be handed back for another. There is a `NOTE` at the eviction site in `world_server.rs`; the fix belongs with the version-bump work that first makes a second version reachable. **Registry keying.** The registry is keyed by version alone. Dimension keying would need a second dimension, which does not exist in the codebase yet. `BTreeMap` over `HashMap` for the determinism rule in `AGENTS.md`. **Type changes worth a reviewer's eye.** `WorldMetadata` loses `Copy` and `Eq` because it now owns a `Vec`. `WorldGenConfig` gains `PartialEq` and `VoxelGenerator` gains `Clone`, the latter so `main.rs` can hand `ServerWorld` the current generator without a second construction. **Scripting layer.** No gameplay logic is involved, so nothing here needs a Lua binding. This is save-format and server-internal plumbing. ### Related Issues _No response_ ### Checklist - [x] I have branched from `dev` (or a feature branch off `dev`) and my PR targets `dev`. - [x] I have kept my changes focused to a single concept. - [x] I have added or updated documentation (`///` doc comments for Rust) where necessary. - [x] I have tested my changes and described any relevant automated or manual testing above.
fix(shared): migrate a v1 level.dat instead of rejecting it
All checks were successful
Auto Labeler / label-scope (pull_request_target) Successful in 4s
CLA Signed All authors have signed the CLA.
CLA Check / cla-check (pull_request_target) Successful in 4s
CI / Rust Check, Lint & Test (pull_request) Successful in 24m43s
CI / Lua Lint & Format (pull_request) Successful in 8s
CI / LFS Pointer Guard (pull_request) Successful in 5s
3950682fe5
decode() rejected any WORLD_FORMAT_VERSION other than the current one,
so a world saved before the worldgen-history change hit
UnsupportedVersion and aborted server startup instead of loading. The
legacy payload only ever recorded a bare worldgen_version, generated
by the one shipped config that predates per-version configs, so that
config now backfills a WorldgenHistory of matching length on load.
docs(save): describe the worldgen history and record ADR-0014
All checks were successful
CLA Signed All authors have signed the CLA.
CLA Check / cla-check (pull_request_target) Successful in 4s
CI / Rust Check, Lint & Test (pull_request) Successful in 24m22s
CI / Lua Lint & Format (pull_request) Successful in 8s
CI / LFS Pointer Guard (pull_request) Successful in 6s
Auto Labeler / label-scope (pull_request_target) Successful in 4s
ab5b5ea0e0
Serkyo deleted branch feat/worldgen-versioning 2026-09-11 23:52:54 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Synvael/synvael!3
No description provided.