feat(scripting): boot a side-aware lua vm with documented bindings and pack loading #11

Merged
Serkyo merged 9 commits from feat/scripting-vm into dev 2026-09-29 13:55:45 +00:00
Owner

What does this PR do?

I replaced the placeholder scripting crate with a working Lua runtime that the base game and mods will load through.

  • ScriptVm (vm.rs) owns one mlua::Lua state for one world session and one Side, Server or Client. It opens the safe standard libraries minus package, creates the global synvael table, and starts in a load phase that ScriptVm::finish_load ends. All state lives in the Lua state, so dropping the VM leaves nothing behind for the next session.
  • api::func (api.rs) is the builder every Lua-exposed function goes through. It takes .doc(), .param(), .returns(), .since(), .server_only() and .load_only(), and build installs the function at its dotted path under synvael while recording a FunctionDoc on the VM. build fails with ScriptError::MissingDoc when .doc() or .since() is missing, and with InvalidPath or PathTaken for bad or occupied paths. A server-only function is not installed on a client VM, but its doc is recorded on both sides. A load-only function raises a Lua error after finish_load.
  • load_pack (pack.rs) runs <pack_root>/scripts/init.lua as the pack's only entry point and treats a missing file as a pack with no Lua tier. It installs a replacement require that resolves <pack_id>.a.b to <pack_root>/scripts/<pack_id>/a/b.lua, caches results per VM, rejects module names that start with another pack's id, and only accepts text chunks. require("synvael") returns the API table.
  • synvael.log.info, .warn and .error (log.rs) forward to tracing with the loading pack's id in a pack field. The global print is the same function as synvael.log.info.
  • ScriptError (error.rs) is the crate's thiserror error type.
  • I renamed assets/scripts/core.lua to assets/scripts/init.lua so the base game pack follows the same entry point rule.
  • I added ADR-0016 for the PUC Lua 5.4 dialect and docs/scripting.md for the VM lifecycle, the builder, side gating, pack loading and logging, and indexed both in docs/README.md.

Why is this change necessary?

ADR-0006 puts the base game on the same Lua API that mods use, and nothing could run a script yet. The crate held a placeholder add function. Content registration, side-gated APIs and the modding tier all need a VM with a defined lifecycle and one way to expose functions before any of them can start.

The dialect had to be settled first because every published mod is written against it. ADR-0016 picks PUC Lua 5.4 over Luau for instruction-count hooks (per-mod CPU budgets), native integers, and the existing LuaCATS type pipeline.

Routing every binding through the builder keeps the API reference generated from the same place the functions are installed, so a function cannot reach Lua undocumented.

Scope of Changes

scripting, workspace, assets

Testing

Automated, all run on the branch head:

  • cargo check -p scripting: passed.
  • cargo test -p scripting: 12 passed, 0 failed. The tests cover side gating and doc recording on both sides, the missing doc and path errors, load-only functions failing after finish_load, pack-rooted require with caching and cross-pack rejection, a pack without init.lua, script errors propagating out of load_pack, a dropped VM leaving no globals, docs or cached modules for the next one, and print being synvael.log.info.
  • cargo clippy --all-targets --all-features -- -D warnings: passed.
  • cargo fmt --all -- --check: passed.
  • stylua --check assets/scripts/ mods/ and selene assets/scripts/ mods/: passed, 0 errors and 0 warnings.

No manual or in-game verification. Neither client nor server depends on scripting yet, so there is no running game path that creates a VM.

Additional Context

The scripting surface is public modding contract from here on: the synvael root table, require naming, scripts/init.lua as the entry point, and synvael.log.

The trust boundary is only structural so far. Server-only functions are absent on the client, package is not loaded and bytecode is rejected, but debug and load are still available, there is no per-pack environment and no CPU budget. Outside load_pack the pack log field is empty until per-pack environments exist.

mlua is built without its send feature, so ScriptVm is !Send and stays on the thread that created it.

The last commit drops the docs/README.md rule requiring every subsystem note to name its design topic. docs/scripting.md is the first note written without that line.

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 replaced the placeholder `scripting` crate with a working Lua runtime that the base game and mods will load through. - `ScriptVm` (`vm.rs`) owns one `mlua::Lua` state for one world session and one `Side`, `Server` or `Client`. It opens the safe standard libraries minus `package`, creates the global `synvael` table, and starts in a load phase that `ScriptVm::finish_load` ends. All state lives in the `Lua` state, so dropping the VM leaves nothing behind for the next session. - `api::func` (`api.rs`) is the builder every Lua-exposed function goes through. It takes `.doc()`, `.param()`, `.returns()`, `.since()`, `.server_only()` and `.load_only()`, and `build` installs the function at its dotted path under `synvael` while recording a `FunctionDoc` on the VM. `build` fails with `ScriptError::MissingDoc` when `.doc()` or `.since()` is missing, and with `InvalidPath` or `PathTaken` for bad or occupied paths. A server-only function is not installed on a client VM, but its doc is recorded on both sides. A load-only function raises a Lua error after `finish_load`. - `load_pack` (`pack.rs`) runs `<pack_root>/scripts/init.lua` as the pack's only entry point and treats a missing file as a pack with no Lua tier. It installs a replacement `require` that resolves `<pack_id>.a.b` to `<pack_root>/scripts/<pack_id>/a/b.lua`, caches results per VM, rejects module names that start with another pack's id, and only accepts text chunks. `require("synvael")` returns the API table. - `synvael.log.info`, `.warn` and `.error` (`log.rs`) forward to `tracing` with the loading pack's id in a `pack` field. The global `print` is the same function as `synvael.log.info`. - `ScriptError` (`error.rs`) is the crate's `thiserror` error type. - I renamed `assets/scripts/core.lua` to `assets/scripts/init.lua` so the base game pack follows the same entry point rule. - I added ADR-0016 for the PUC Lua 5.4 dialect and `docs/scripting.md` for the VM lifecycle, the builder, side gating, pack loading and logging, and indexed both in `docs/README.md`. ### Why is this change necessary? ADR-0006 puts the base game on the same Lua API that mods use, and nothing could run a script yet. The crate held a placeholder `add` function. Content registration, side-gated APIs and the modding tier all need a VM with a defined lifecycle and one way to expose functions before any of them can start. The dialect had to be settled first because every published mod is written against it. ADR-0016 picks PUC Lua 5.4 over Luau for instruction-count hooks (per-mod CPU budgets), native integers, and the existing LuaCATS type pipeline. Routing every binding through the builder keeps the API reference generated from the same place the functions are installed, so a function cannot reach Lua undocumented. ### Scope of Changes scripting, workspace, assets ### Testing Automated, all run on the branch head: - `cargo check -p scripting`: passed. - `cargo test -p scripting`: 12 passed, 0 failed. The tests cover side gating and doc recording on both sides, the missing doc and path errors, load-only functions failing after `finish_load`, pack-rooted `require` with caching and cross-pack rejection, a pack without `init.lua`, script errors propagating out of `load_pack`, a dropped VM leaving no globals, docs or cached modules for the next one, and `print` being `synvael.log.info`. - `cargo clippy --all-targets --all-features -- -D warnings`: passed. - `cargo fmt --all -- --check`: passed. - `stylua --check assets/scripts/ mods/` and `selene assets/scripts/ mods/`: passed, 0 errors and 0 warnings. No manual or in-game verification. Neither `client` nor `server` depends on `scripting` yet, so there is no running game path that creates a VM. ### Additional Context The scripting surface is public modding contract from here on: the `synvael` root table, `require` naming, `scripts/init.lua` as the entry point, and `synvael.log`. The trust boundary is only structural so far. Server-only functions are absent on the client, `package` is not loaded and bytecode is rejected, but `debug` and `load` are still available, there is no per-pack environment and no CPU budget. Outside `load_pack` the `pack` log field is empty until per-pack environments exist. `mlua` is built without its `send` feature, so `ScriptVm` is `!Send` and stays on the thread that created it. The last commit drops the `docs/README.md` rule requiring every subsystem note to name its design topic. `docs/scripting.md` is the first note written without that line. ### 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.
docs(workspace): drop the design source naming rule for subsystem notes
Some checks failed
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 / 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
CI / Rust Check, Lint & Test (pull_request) Has been cancelled
b7f6b6409e
chore(scripting): drop the unused shared dependency
All checks were successful
CLA Signed All authors have signed the CLA.
CLA Check / cla-check (pull_request_target) Successful in 3s
CI / Rust Check, Lint & Test (pull_request) Successful in 35m10s
CI / Dependency Licenses & Advisories (pull_request) Successful in 31s
CI / Lua Lint & Format (pull_request) Successful in 7s
CI / LFS Pointer Guard (pull_request) Successful in 5s
CI / Commit Message Lint (pull_request) Successful in 2s
d8141e4e5b
ci(workspace): verify the selene download against a pinned checksum
All checks were successful
CLA Signed All authors have signed the CLA.
CLA Check / cla-check (pull_request_target) Successful in 3s
CI / Rust Check, Lint & Test (pull_request) Successful in 36m4s
CI / Dependency Licenses & Advisories (pull_request) Successful in 32s
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
f5a4237f9d
Serkyo deleted branch feat/scripting-vm 2026-09-29 13:55:45 +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!11
No description provided.