feat(shared): intern namespaced content ids in a freezable registry #9

Merged
Serkyo merged 4 commits from feat/content-ids into dev 2026-09-27 14:12:03 +00:00
Owner

What does this PR do?

I added a registry module to crates/shared that implements the content ID format from ADR-0005 and interns those IDs to handles.

  • ContentId in crates/shared/src/registry.rs is a validated namespace:name string. Both halves must match [a-z0-9_-]+ with exactly one : between them. A bare name returns ContentIdError::MissingNamespace and is never coerced to syn:. Validation runs in TryFrom<String>, TryFrom<&str> and FromStr, and serde goes through try_from = "String", so a deserialised ID is validated too. It serialises as its canonical string in both JSON and postcard.
  • ItemId is a new #[repr(transparent)] u16 handle deriving Pod and Zeroable, matching BlockId. It lives in registry because items are not voxels; BlockId stays in world.
  • RegistryBuilder<H, D> assigns dense u16 handles in registration order and keeps a BTreeMap<ContentId, H> for reverse lookup. register returns RegistryError::Duplicate for an ID already present and RegistryError::Exhausted once all 65536 handles are taken. freeze consumes the builder and returns a Registry<H, D>, which only has get, id and handle, so nothing can register after load.
  • RegistryBuilder::<BlockId, _>::with_air pre-registers syn:air at BlockId::AIR, since chunks treat handle 0 as empty space. Block handles therefore start at 1 and item handles (RegistryBuilder::<ItemId, _>::new) start at 0.
  • ContentHandle is a sealed trait implemented for BlockId and ItemId only, so the builder is generic over the two handle types without being open to arbitrary ones.
  • The error enums use thiserror and live in crates/shared/src/registry/error.rs.

Why is this change necessary?

ADR-0005 fixed the namespace:id format and the interning step, but none of it existed in code. BlockId values are still hand-assigned and nothing validates an identifier. Worldgen data, the Lua API and save files all need a single place that parses IDs and maps them to handles before any of them can move off raw integers.

Scope of Changes

shared

Testing

Automated:

  • cargo check -p shared: passes.
  • cargo test -p shared: 269 passed, 0 failed.
  • cargo clippy --all-targets --all-features -- -D warnings: passes.
  • cargo fmt --all -- --check: passes.

New tests in crates/shared/src/tests/registry.rs cover well-formed IDs, the namespace and name split, rejection of bare names, empty halves, a second :, and characters outside the charset (uppercase, spaces, dots, slashes, non-ASCII). They also check JSON and postcard round trips, that deserialising an invalid string fails, air at handle 0, dense handle assignment for blocks and items, lookups in both directions on a frozen registry, duplicate rejection, and exhaustion after BlockId(u16::MAX).

Manual: none. Nothing outside the tests uses the registry yet, so there is no in-game behaviour to check.

Additional Context

This PR only adds the types. Nothing is wired to them yet: worldgen config still holds raw BlockId values, the save format is unchanged, and there is no Lua binding. Those move over in follow-up PRs.

Handles depend on registration order, and ChunkData still stores BlockId raw. Until a world carries its own namespace:id to handle table, changing the registered content set would reinterpret saved blocks. ADR-0005 already records this.

Registering the same ID twice is an error even when the definitions match, because there is no rule yet for when two definitions count as equal.

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 a `registry` module to `crates/shared` that implements the content ID format from ADR-0005 and interns those IDs to handles. - `ContentId` in `crates/shared/src/registry.rs` is a validated `namespace:name` string. Both halves must match `[a-z0-9_-]+` with exactly one `:` between them. A bare name returns `ContentIdError::MissingNamespace` and is never coerced to `syn:`. Validation runs in `TryFrom<String>`, `TryFrom<&str>` and `FromStr`, and serde goes through `try_from = "String"`, so a deserialised ID is validated too. It serialises as its canonical string in both JSON and postcard. - `ItemId` is a new `#[repr(transparent)]` `u16` handle deriving `Pod` and `Zeroable`, matching `BlockId`. It lives in `registry` because items are not voxels; `BlockId` stays in `world`. - `RegistryBuilder<H, D>` assigns dense `u16` handles in registration order and keeps a `BTreeMap<ContentId, H>` for reverse lookup. `register` returns `RegistryError::Duplicate` for an ID already present and `RegistryError::Exhausted` once all 65536 handles are taken. `freeze` consumes the builder and returns a `Registry<H, D>`, which only has `get`, `id` and `handle`, so nothing can register after load. - `RegistryBuilder::<BlockId, _>::with_air` pre-registers `syn:air` at `BlockId::AIR`, since chunks treat handle 0 as empty space. Block handles therefore start at 1 and item handles (`RegistryBuilder::<ItemId, _>::new`) start at 0. - `ContentHandle` is a sealed trait implemented for `BlockId` and `ItemId` only, so the builder is generic over the two handle types without being open to arbitrary ones. - The error enums use `thiserror` and live in `crates/shared/src/registry/error.rs`. ### Why is this change necessary? ADR-0005 fixed the `namespace:id` format and the interning step, but none of it existed in code. `BlockId` values are still hand-assigned and nothing validates an identifier. Worldgen data, the Lua API and save files all need a single place that parses IDs and maps them to handles before any of them can move off raw integers. ### Scope of Changes shared ### Testing Automated: - `cargo check -p shared`: passes. - `cargo test -p shared`: 269 passed, 0 failed. - `cargo clippy --all-targets --all-features -- -D warnings`: passes. - `cargo fmt --all -- --check`: passes. New tests in `crates/shared/src/tests/registry.rs` cover well-formed IDs, the namespace and name split, rejection of bare names, empty halves, a second `:`, and characters outside the charset (uppercase, spaces, dots, slashes, non-ASCII). They also check JSON and postcard round trips, that deserialising an invalid string fails, air at handle 0, dense handle assignment for blocks and items, lookups in both directions on a frozen registry, duplicate rejection, and exhaustion after `BlockId(u16::MAX)`. Manual: none. Nothing outside the tests uses the registry yet, so there is no in-game behaviour to check. ### Additional Context This PR only adds the types. Nothing is wired to them yet: worldgen config still holds raw `BlockId` values, the save format is unchanged, and there is no Lua binding. Those move over in follow-up PRs. Handles depend on registration order, and `ChunkData` still stores `BlockId` raw. Until a world carries its own `namespace:id` to handle table, changing the registered content set would reinterpret saved blocks. ADR-0005 already records this. Registering the same ID twice is an error even when the definitions match, because there is no rule yet for when two definitions count as equal. ### 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.
fix(shared): reserve block handle 0 for air in the registry
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 33m59s
CI / Dependency Licenses & Advisories (pull_request) Successful in 26s
CI / Lua Lint & Format (pull_request) Successful in 6s
CI / Commit Message Lint (pull_request) Successful in 2s
CI / LFS Pointer Guard (pull_request) Successful in 5s
d05e37164f
Serkyo deleted branch feat/content-ids 2026-09-27 14:12:04 +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!9
No description provided.