feat/block-state-sidetable #6

Merged
Serkyo merged 4 commits from feat/block-state-sidetable into dev 2026-09-24 16:51:44 +00:00
Owner

What does this PR do?

I added BlockStateTable, a sparse per-voxel side-band state container in crates/shared/src/world/block_state.rs. It maps a local voxel index to an opaque byte payload capped at MAX_BLOCK_STATE_PAYLOAD_LEN (256 bytes), backed by a BTreeMap so iteration and serialization stay deterministic.

I wired the table into ChunkData as a new block_state field, exposed through block_state() and block_state_mut(). The field is #[serde(skip)] because the SYNC record frames it as its own section rather than folding it into the edit-set payload, so a chunk carrying no state costs nothing extra.

I extended the SYNC record format (crates/shared/src/save/record.rs) to carry that second section: two more length fields in the header, an optional compressed block-state payload after the chunk payload, and a bump of CHUNK_FORMAT_VERSION to 2. encode/decode share a compress_section/decompress_section pair now instead of duplicating the zstd/postcard logic inline. decode rejects any record whose version doesn't match the current one rather than guessing at an older layout, since there's no migrator yet.

I moved BlockStateError out into a dedicated crates/shared/src/world/error.rs module and wired it into SaveError via #[from], so a decode-time size violation on the side table surfaces as SaveError::BlockState.

Why is this change necessary?

Gameplay features like fluid levels, redstone-equivalent wiring, or growth stages need per-voxel state that doesn't fit the existing single-block-id edit model, without paying the cost of widening every chunk's edit set for voxels that never use it. This lays the storage and persistence groundwork so the Lua API can build state-bearing block types on top of it later.

Scope of Changes

shared

Testing

Automated:

  • cargo check -p shared
  • cargo test -p shared (241 passed, 0 failed)
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check

New tests cover BlockStateTable insert/remove/iteration/validation, the oversized-payload rejection path both on insert and on data deserialized straight from bytes, and record round-tripping with a populated and an empty side table, an unsupported version rejection, and length-mismatch handling on the new section.

No manual or in-game verification was done; this is storage/format-only and isn't reachable from gameplay yet.

Additional Context

CHUNK_FORMAT_VERSION moved from 1 to 2. There's no migrator for existing saves, consistent with the project's current policy of bumping format versions without one until a world exists off the dev machine. Any SYNC record written under version 1 will now fail to decode with SaveError::UnsupportedVersion.

ChunkData::is_unmodified does not consider block_state: a chunk whose voxel edits match the baseline but whose state table is populated is a dirtiness case no caller produces yet, so I left it unhandled rather than guessing at the right behavior.

None.

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? I added `BlockStateTable`, a sparse per-voxel side-band state container in `crates/shared/src/world/block_state.rs`. It maps a local voxel index to an opaque byte payload capped at `MAX_BLOCK_STATE_PAYLOAD_LEN` (256 bytes), backed by a `BTreeMap` so iteration and serialization stay deterministic. I wired the table into `ChunkData` as a new `block_state` field, exposed through `block_state()` and `block_state_mut()`. The field is `#[serde(skip)]` because the `SYNC` record frames it as its own section rather than folding it into the edit-set payload, so a chunk carrying no state costs nothing extra. I extended the `SYNC` record format (`crates/shared/src/save/record.rs`) to carry that second section: two more length fields in the header, an optional compressed block-state payload after the chunk payload, and a bump of `CHUNK_FORMAT_VERSION` to 2. `encode`/`decode` share a `compress_section`/`decompress_section` pair now instead of duplicating the zstd/postcard logic inline. `decode` rejects any record whose version doesn't match the current one rather than guessing at an older layout, since there's no migrator yet. I moved `BlockStateError` out into a dedicated `crates/shared/src/world/error.rs` module and wired it into `SaveError` via `#[from]`, so a decode-time size violation on the side table surfaces as `SaveError::BlockState`. ### Why is this change necessary? Gameplay features like fluid levels, redstone-equivalent wiring, or growth stages need per-voxel state that doesn't fit the existing single-block-id edit model, without paying the cost of widening every chunk's edit set for voxels that never use it. This lays the storage and persistence groundwork so the Lua API can build state-bearing block types on top of it later. ### Scope of Changes shared ### Testing Automated: - `cargo check -p shared` - `cargo test -p shared` (241 passed, 0 failed) - `cargo clippy --all-targets --all-features -- -D warnings` - `cargo fmt --all -- --check` New tests cover `BlockStateTable` insert/remove/iteration/validation, the oversized-payload rejection path both on `insert` and on data deserialized straight from bytes, and record round-tripping with a populated and an empty side table, an unsupported version rejection, and length-mismatch handling on the new section. No manual or in-game verification was done; this is storage/format-only and isn't reachable from gameplay yet. ### Additional Context `CHUNK_FORMAT_VERSION` moved from 1 to 2. There's no migrator for existing saves, consistent with the project's current policy of bumping format versions without one until a world exists off the dev machine. Any `SYNC` record written under version 1 will now fail to decode with `SaveError::UnsupportedVersion`. `ChunkData::is_unmodified` does not consider `block_state`: a chunk whose voxel edits match the baseline but whose state table is populated is a dirtiness case no caller produces yet, so I left it unhandled rather than guessing at the right behavior. ### Related Issues None. ### 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.
refactor(shared): move BlockStateError into a dedicated world error module
All checks were successful
Auto Labeler / label-scope (pull_request_target) Successful in 2s
CLA Signed All authors have signed the CLA.
CLA Check / cla-check (pull_request_target) Successful in 2s
CI / Rust Check, Lint & Test (pull_request) Successful in 36m8s
CI / Dependency Licenses & Advisories (pull_request) Successful in 28s
CI / Lua Lint & Format (pull_request) Successful in 7s
CI / Commit Message Lint (pull_request) Successful in 2s
CI / LFS Pointer Guard (pull_request) Successful in 5s
87c0edf01b
Serkyo deleted branch feat/block-state-sidetable 2026-09-24 16:51:44 +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!6
No description provided.