feat(scripting): load base game blocks from data files into worldgen #13

Merged
Serkyo merged 6 commits from feat/data-loader-worldgen into dev 2026-10-01 12:40:41 +00:00
Owner

What does this PR do?

Content is now registered only from data files, per the new ADR-0017, which supersedes the registration path of ADR-0007. I removed synvael.blocks.register and synvael.items.register from the Lua API, so scripts refer to content by namespace:id and never define it.

I added a data-pack loader in crates/scripting/src/data.rs. load_data reads every <pack_root>/data/blocks/*.json and data/items/*.json file in file-name order and deserialises each one straight into BlockDef or ItemDef, with no Lua involved. crates/scripting/src/content.rs then registers it after checking the load phase and that the ID uses the loading pack's namespace. A file that fails to parse or register returns the new ScriptError::Data naming the file, with a DataError saying why. A client VM skips data/ since content is authoritative. The mlua serialize feature is no longer needed and is dropped.

load_pack now sets the loading-pack scope once and loads data/ before scripts/init.lua, so a script can refer to blocks its pack declared as data. I added scripting::load_base_game(side, assets_root), which creates a VM, loads assets/ as pack syn, ends the load phase, and returns the unbound Content.

The base game's blocks now live in assets/data/blocks/dirt.json, grass.json and stone.json.

In shared, WorldGenConfig names its three blocks by ContentId instead of raw BlockId. VoxelGenerator::new takes the block registry and resolves the IDs to handles once, returning GeneratorError::UnregisteredBlock for an ID the registry does not hold. assets/data/worldgen/default/0.json now says "syn:grass", "syn:dirt" and "syn:stone". Because the worldgen params stored in level.dat changed shape, I bumped WORLD_FORMAT_VERSION from 3 to 4.

In server/src/main.rs I removed the hardcoded BLOCK_IDS list and register_blocks. The server now gets its blocks from scripting::load_base_game, and build_generators builds one generator per worldgen history entry against the bound registry. inspect_save logs the params field by field so the IDs print readably.

Why is this change necessary?

The server registered blocks from a Rust constant, and worldgen referred to them by raw handles 1 to 3 that only matched because the constant happened to register them in that order. That bypassed the scripting layer, and ADR-0007 requires content to be declared through the modding API. Content could also have been written either as JSON or through the Lua register functions from PR #12, which gave each content kind two authoring formats to keep in sync. ADR-0017 keeps one: data files, with datagen (not part of this PR) as the only way to generate them programmatically. Naming blocks by content ID in the worldgen params also means a history entry keeps its meaning whatever handles a world's ID table assigns.

Scope of Changes

server, shared, scripting, workspace, assets

Testing

Automated, all passing:

  • cargo check -p scripting -p server -p shared
  • cargo test -p scripting -p server -p shared
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check

New tests cover block and item data files registering in file-name order, malformed and rejected files erroring with the file name, each kind of refused registration (missing or foreign-namespace ID, unknown field, registration after load), Lua having no registration functions, the base game registering its blocks at the handles worldgen expects, a generator refusing a config that names an unregistered block, a block the base game no longer registers staying an orphan in the ID table, and a version 3 level.dat being refused.

Manual: I ran both the client and the server with --release on my machine.

Additional Context

This breaks the save format. WORLD_FORMAT_VERSION is now 4 and a version 3 level.dat is refused with UnsupportedVersion per ADR-0015, so existing dev worlds have to be deleted. The worldgen config schema also changed: block fields are now content ID strings, not integers. docs/save_format.md and docs/scripting.md describe both.

This also removes synvael.blocks.register and synvael.items.register. They were added in PR #12 and never shipped, and no script in assets/ or mods/ calls them. docs/scripting.md, docs/packs.md and DEVELOPMENT.md are updated.

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? Content is now registered only from data files, per the new ADR-0017, which supersedes the registration path of ADR-0007. I removed `synvael.blocks.register` and `synvael.items.register` from the Lua API, so scripts refer to content by `namespace:id` and never define it. I added a data-pack loader in `crates/scripting/src/data.rs`. `load_data` reads every `<pack_root>/data/blocks/*.json` and `data/items/*.json` file in file-name order and deserialises each one straight into `BlockDef` or `ItemDef`, with no Lua involved. `crates/scripting/src/content.rs` then registers it after checking the load phase and that the ID uses the loading pack's namespace. A file that fails to parse or register returns the new `ScriptError::Data` naming the file, with a `DataError` saying why. A client VM skips `data/` since content is authoritative. The mlua `serialize` feature is no longer needed and is dropped. `load_pack` now sets the loading-pack scope once and loads `data/` before `scripts/init.lua`, so a script can refer to blocks its pack declared as data. I added `scripting::load_base_game(side, assets_root)`, which creates a VM, loads `assets/` as pack `syn`, ends the load phase, and returns the unbound `Content`. The base game's blocks now live in `assets/data/blocks/dirt.json`, `grass.json` and `stone.json`. In `shared`, `WorldGenConfig` names its three blocks by `ContentId` instead of raw `BlockId`. `VoxelGenerator::new` takes the block registry and resolves the IDs to handles once, returning `GeneratorError::UnregisteredBlock` for an ID the registry does not hold. `assets/data/worldgen/default/0.json` now says `"syn:grass"`, `"syn:dirt"` and `"syn:stone"`. Because the worldgen params stored in `level.dat` changed shape, I bumped `WORLD_FORMAT_VERSION` from 3 to 4. In `server/src/main.rs` I removed the hardcoded `BLOCK_IDS` list and `register_blocks`. The server now gets its blocks from `scripting::load_base_game`, and `build_generators` builds one generator per worldgen history entry against the bound registry. `inspect_save` logs the params field by field so the IDs print readably. ### Why is this change necessary? The server registered blocks from a Rust constant, and worldgen referred to them by raw handles 1 to 3 that only matched because the constant happened to register them in that order. That bypassed the scripting layer, and ADR-0007 requires content to be declared through the modding API. Content could also have been written either as JSON or through the Lua `register` functions from PR #12, which gave each content kind two authoring formats to keep in sync. ADR-0017 keeps one: data files, with datagen (not part of this PR) as the only way to generate them programmatically. Naming blocks by content ID in the worldgen params also means a history entry keeps its meaning whatever handles a world's ID table assigns. ### Scope of Changes server, shared, scripting, workspace, assets ### Testing Automated, all passing: - `cargo check -p scripting -p server -p shared` - `cargo test -p scripting -p server -p shared` - `cargo clippy --all-targets --all-features -- -D warnings` - `cargo fmt --all -- --check` New tests cover block and item data files registering in file-name order, malformed and rejected files erroring with the file name, each kind of refused registration (missing or foreign-namespace ID, unknown field, registration after load), Lua having no registration functions, the base game registering its blocks at the handles worldgen expects, a generator refusing a config that names an unregistered block, a block the base game no longer registers staying an orphan in the ID table, and a version 3 `level.dat` being refused. Manual: I ran both the client and the server with `--release` on my machine. ### Additional Context This breaks the save format. `WORLD_FORMAT_VERSION` is now 4 and a version 3 `level.dat` is refused with `UnsupportedVersion` per ADR-0015, so existing dev worlds have to be deleted. The worldgen config schema also changed: block fields are now content ID strings, not integers. `docs/save_format.md` and `docs/scripting.md` describe both. This also removes `synvael.blocks.register` and `synvael.items.register`. They were added in PR #12 and never shipped, and no script in `assets/` or `mods/` calls them. `docs/scripting.md`, `docs/packs.md` and `DEVELOPMENT.md` are updated. ### 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.
feat(server): boot the base game through the scripting layer
Some checks failed
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) Has been cancelled
CI / Dependency Licenses & Advisories (pull_request) Has been cancelled
CI / Lua Lint & Format (pull_request) Has been cancelled
CI / Commit Message Lint (pull_request) Has been cancelled
CI / LFS Pointer Guard (pull_request) Has been cancelled
Auto Labeler / label-scope (pull_request_target) Successful in 3s
4ce1e64271
Serkyo changed title from feat(scripting): load base game blocks from data files into worldgen to WIP: feat(scripting): load base game blocks from data files into worldgen 2026-09-30 13:08:58 +00:00
docs(workspace): describe data-only content registration
All checks were successful
CI / Rust Check, Lint & Test (pull_request) Successful in 35m42s
CLA Signed All authors have signed the CLA.
CLA Check / cla-check (pull_request_target) Successful in 2s
CI / Dependency Licenses & Advisories (pull_request) Successful in 29s
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
Auto Labeler / label-scope (pull_request_target) Successful in 2s
35db23f5d9
Serkyo changed title from WIP: feat(scripting): load base game blocks from data files into worldgen to feat(scripting): load base game blocks from data files into worldgen 2026-09-30 13:23:23 +00:00
Serkyo deleted branch feat/data-loader-worldgen 2026-10-01 12:40:41 +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!13
No description provided.