Add optional Lua scripting module, examples, and validation
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
Lua 5.4.9 — MIT License
|
||||
https://www.lua.org/license.html
|
||||
|
||||
Copyright (C) 1994-2026 Lua.org, PUC-Rio.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -40,13 +40,32 @@ build/linux-debug/faset_editor --project "$PWD/MyGame" --new MyGame --dimension
|
||||
You can also run `build/linux-debug/faset_editor` without arguments to open the
|
||||
project launcher and create or select a project using the native interface.
|
||||
|
||||
Use **Build C++** after changing `MyGame/Scripts/Gameplay.cpp`, then **Play**.
|
||||
Use **Build** after changing `MyGame/Scripts/Gameplay.cpp`, then **Play**.
|
||||
The Player runs separately. Stop it before changing and rebuilding C++ gameplay.
|
||||
See [MCP and CLI](../editor/mcp.md) for headless authoring and automation.
|
||||
|
||||
For an optimized build use `linux-release`. The `linux-sanitize` preset enables
|
||||
AddressSanitizer and UndefinedBehaviorSanitizer for tests without the graphics backend.
|
||||
|
||||
## Optional Lua module
|
||||
|
||||
Engine development builds enable `FASET_ENABLE_LUA` by default. Lua 5.4.9 is compiled
|
||||
from its checksum-pinned source archive; no system Lua installation is required.
|
||||
Pass `-DFASET_ENABLE_LUA=OFF` to omit the VM and bindings. The Editor's project
|
||||
build/export service selects this flag from `scripting.lua.scripts` in
|
||||
`project.faset.json`, so C++-only games do not link Lua.
|
||||
|
||||
See the [Lua guide](../scripting/lua.md) for the manifest, a Lua-only project,
|
||||
hot reload, and external-editor/LuaLS setup. Headless CPU checks can be run with:
|
||||
|
||||
```sh
|
||||
cmake --preset linux-debug -DFASET_BUILD_RENDERER=OFF -DFASET_BUILD_EDITOR=OFF
|
||||
cmake --build --preset linux-debug --parallel
|
||||
ctest --preset linux-debug
|
||||
```
|
||||
|
||||
These checks do not verify the graphical Player or renderer.
|
||||
|
||||
## Dependencies and offline builds
|
||||
|
||||
Dependency source URLs, commits, and archive SHA-256 values are stored in
|
||||
|
||||
@@ -9,10 +9,11 @@ and how those functions interact with scenes, physics, and the editor.
|
||||
Blender import and standalone Linux/Windows export. Acceptance used a physical
|
||||
Linux GPU and software Vulkan on Windows. See the
|
||||
[acceptance dossier](https://github.com/emil28092005/Faset_Engine/blob/main/docs/validation/mvp-acceptance.md)
|
||||
for exact source revisions and coverage limits. Lua and advanced graphics remain
|
||||
later milestones.
|
||||
for exact source revisions and coverage limits. The optional Lua module is
|
||||
documented separately; those historical acceptance results do not certify later
|
||||
changes. Advanced graphics remain later milestones.
|
||||
|
||||
Start with [how C++ gameplay works](scripting/index.md), then read
|
||||
Start with [how gameplay works](scripting/index.md), then read
|
||||
[frame and physics updates](scripting/lifecycle.md). See
|
||||
[Build from source](getting-started/build.md) for the toolchain and build commands.
|
||||
|
||||
@@ -30,7 +31,9 @@ The manual grows alongside tested engine capabilities, in this order:
|
||||
5. Work with scene templates, assets, and references.
|
||||
6. Import from Blender and export a standalone game.
|
||||
|
||||
Lua is planned after the C++ foundation. It is not a current scripting option.
|
||||
For interpreted gameplay, follow [Lua gameplay](scripting/lua.md): declare component
|
||||
fields, write callbacks, and reload scripts during development Play. The Lua-only
|
||||
example in `examples/lua` uses the same physics and scene model as the C++ tutorials.
|
||||
|
||||
## Preview this manual
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# C++ gameplay
|
||||
# Gameplay scripting
|
||||
|
||||
In Faset, a gameplay script is **C++ compiled into the Player**. You write ordinary functions and register the callbacks an object needs. There is no C++ interpreter or live replacement of compiled classes. Stop Play, rebuild, export the schema, and start a new Player session.
|
||||
Faset supports **C++ compiled into the Player** and optional [Lua gameplay](lua.md).
|
||||
Both languages register component schemas and callbacks on the same runtime.
|
||||
For C++, there is no interpreter or live replacement of compiled classes: stop Play,
|
||||
rebuild, export the schema, and start a new Player session.
|
||||
|
||||
Lua is planned for a later stage. The APIs and tutorials in this section describe the C++ implementation available now.
|
||||
The following tutorials describe C++. See [Lua gameplay](lua.md) for Lua-only or
|
||||
mixed projects, the Lua API, source reload, and external-editor completion.
|
||||
|
||||
## Start here
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
# Lua gameplay
|
||||
|
||||
Faset embeds **Lua 5.4.9** as an optional gameplay module. Lua and compiled C++
|
||||
behaviors share the same runtime lifecycle, typed entity operations, scene components,
|
||||
and Inspector metadata. The Editor does not run gameplay code in its own process.
|
||||
There is no built-in script editor: edit `.lua` files in Zed or another external editor.
|
||||
|
||||
## Enable Lua in a project
|
||||
|
||||
Add explicit entry scripts to `project.faset.json`:
|
||||
|
||||
```json
|
||||
"scripting": {
|
||||
"lua": {
|
||||
"scripts": ["Scripts/player.lua", "Scripts/beacon.lua"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a manifest fragment, not a complete project file. Each entry must return one
|
||||
`faset.behavior` table with a unique custom TypeId. All sources live beneath `Scripts`
|
||||
and are captured as an immutable build/export snapshot. Paths must be project-relative;
|
||||
symlinks and paths outside `Scripts` are rejected. Auxiliary modules do not need to
|
||||
appear in the entry list.
|
||||
|
||||
A Lua-only project can omit both `Scripts/Gameplay.cpp` and `Scripts/Gameplay.hpp`.
|
||||
A mixed project keeps that pair and adds the Lua declaration. TypeIds must be unique
|
||||
across both languages, and the `faset.*` namespace is reserved for native components.
|
||||
|
||||
The engine developer option `FASET_ENABLE_LUA` defaults to `ON`. Project builds select
|
||||
it from the manifest, so a C++-only game does not link the Lua VM. Lua is pinned and
|
||||
built from source; no system Lua installation is required.
|
||||
|
||||
The complete `examples/lua` project includes a playable
|
||||
2D controller, a non-physical animated beacon, and a shared module. Its scripts are
|
||||
also loaded by the Lua contract test.
|
||||
|
||||
## Write a behavior
|
||||
|
||||
```lua
|
||||
local Player = faset.behavior {
|
||||
id = "game.player",
|
||||
version = 1,
|
||||
name = "Player",
|
||||
fields = {
|
||||
speed = {
|
||||
name = "Move speed", type = "number", default = 5,
|
||||
min = 0, max = 30, units = "m/s"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Player:on_start()
|
||||
self.state.elapsed = 0
|
||||
end
|
||||
|
||||
function Player:fixed_update(delta)
|
||||
self.state.elapsed = self.state.elapsed + delta
|
||||
local velocity = self.entity:velocity()
|
||||
velocity.x = faset.input().horizontal * self.fields.speed
|
||||
self.entity:set_velocity(velocity)
|
||||
end
|
||||
|
||||
return Player
|
||||
```
|
||||
|
||||
Attach a component with `type: "game.player"`, `version: 1`, and the desired field
|
||||
overrides to an entity with a 2D or 3D rigid body. Refresh schemas to expose the
|
||||
behavior in **Add Component** and its `speed` field in the Inspector. Saved scenes
|
||||
store the stable TypeId and data, not an instance of a Lua object.
|
||||
|
||||
Each entity/component gets its own instance:
|
||||
|
||||
- `self.entity`: an opaque runtime handle, checked on every call.
|
||||
- `self.fields`: a configuration copy, combining schema defaults and scene overrides.
|
||||
- `self.state`: a fresh mutable table for counters, timers, and retained handles.
|
||||
|
||||
Changing either table does not modify the saved scene or create an Undo operation.
|
||||
Module-local variables are shared by instances of that module; put per-entity state
|
||||
in `self.state`. Lua tables returned by getters are copies, not native pointers.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Use colon definitions so Lua supplies `self`:
|
||||
|
||||
| Callback | When it runs |
|
||||
|---|---|
|
||||
| `on_start()` | Once after the instance and initial scene objects exist |
|
||||
| `fixed_update(delta)` | Before each fixed physics step; delta is seconds |
|
||||
| `on_collision(event)` | After physics, for contact begin/end |
|
||||
| `update(delta)` | Once per rendered frame after fixed steps |
|
||||
| `late_update(delta)` | After presentation interpolation |
|
||||
| `on_destroy()` | Before component/entity removal, while the handle is still valid |
|
||||
|
||||
Omit unused callbacks. The same [timing rules](lifecycle.md) as C++ apply, including
|
||||
input edges, fixed-tick catch-up, deferred structural changes, pause and single-step.
|
||||
Do not multiply velocity by delta; multiply a manually calculated displacement.
|
||||
|
||||
An error is reported with source location/traceback and disables the offending
|
||||
instance for that generation and releases its instance state. Other instances can continue.
|
||||
The VM quota is shared: allocations retained by module-level variables can still
|
||||
affect other behaviors. A restart/reload creates
|
||||
fresh instances; disabled instances are not automatically retried every frame.
|
||||
Changes already made or queued by a failing callback are not rolled back.
|
||||
|
||||
## Runtime API
|
||||
|
||||
`faset.find("scene-id")` returns an entity handle or `nil`. Handles support equality
|
||||
and `:valid()`. Retained handles become invalid after destruction or scene restart;
|
||||
calling other methods on a stale handle reports an error.
|
||||
|
||||
| Entity method | Contract |
|
||||
|---|---|
|
||||
| `:transform()` / `:presentation()` | Copy of simulation/display transform |
|
||||
| `:set_transform(pose)` | Non-physical objects only |
|
||||
| `:set_presentation(pose)` | Display-only write during `late_update` |
|
||||
| `:teleport(pose)` | Explicit discontinuous pose change; preserves velocity |
|
||||
| `:fields(type_id)` | Copy of the named component's stored fields |
|
||||
| `:velocity()` / `:set_velocity(v)` | Linear velocity, rigid bodies only |
|
||||
| `:apply_impulse(v)` | Impulse at the rigid body's centre |
|
||||
| `:is_grounded()` | Support from completed native physics contacts |
|
||||
| `:destroy()` | Queue entity/descendant removal |
|
||||
| `:add_component(record)` | Queue a complete component record |
|
||||
| `:remove_component(type_id)` | Queue component removal |
|
||||
|
||||
Typed vectors are `{x = 1, y = 2, z = 0}`. Transforms contain `position`, `rotation`,
|
||||
and `scale`, each a named vector. Positions use metres; rotations use XYZ Euler
|
||||
radians. In contrast, **scene/component JSON arrays** are represented as ordinary
|
||||
1-based Lua arrays, such as `fields.position = {1, 2, 0}`. Use `faset.null` to retain
|
||||
an explicit JSON null; Lua `nil` removes a table key.
|
||||
An empty Lua table converts to a JSON object; an empty schema default with
|
||||
`type = "array"` is normalized to an empty JSON array.
|
||||
|
||||
`faset.input()` returns `horizontal`, `vertical`, `jump_pressed`, and
|
||||
`interact_pressed`. Player mappings are A/D or arrows, W/S or arrows, Space, and E.
|
||||
`faset.log(...)` sends a bounded message to Player logs and the Editor Console.
|
||||
|
||||
Collision events contain `first`, `second`, `other` (the opposite entity), and `began`.
|
||||
They are copied for Lua, but retained entity handles still need validity checks.
|
||||
|
||||
`faset.spawn(record)` queues a full scene entity record. It returns **no handle**:
|
||||
use `faset.find(id)` after the next fixed-tick barrier. Spawn, destroy, add and remove
|
||||
operations follow FIFO order and do not mutate Editor documents. For example:
|
||||
|
||||
```lua
|
||||
faset.spawn {
|
||||
id = "effect-1", name = "Effect", parent = faset.null,
|
||||
components = {
|
||||
{
|
||||
id = "effect-transform", type = "faset.transform", version = 1,
|
||||
fields = { position = {0, 2, 0} }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Shared modules and sandbox
|
||||
|
||||
`require("util.motion")` resolves `Scripts/util/motion.lua`, then
|
||||
`Scripts/util/motion/init.lua`, inside the captured source snapshot. A module is
|
||||
evaluated once and its result cached within the VM. Missing modules, cycles, and
|
||||
path-like names are errors. There is no native module search, package installation,
|
||||
network access, or arbitrary file access.
|
||||
|
||||
Basic Lua operations and the `math`, `string`, `table`, and `utf8` libraries are
|
||||
available. `io`, `os`, `debug`, dynamic `load`, `loadfile`, `dofile`, `pcall`, `xpcall`,
|
||||
`setmetatable`, `collectgarbage`, `string.dump`, and coroutines are not exposed.
|
||||
Engine-owned metatables are locked; arbitrary finalizers cannot run during shutdown.
|
||||
The restricted API intentionally prevents scripts
|
||||
from catching execution-limit errors and continuing indefinitely.
|
||||
|
||||
The VM has memory and instruction budgets (`LuaLimits`, default 16 MiB and one
|
||||
million instructions per protected entry/callback). These are gameplay reliability
|
||||
limits, not a promise that executing untrusted code is equivalent to OS isolation.
|
||||
JSON conversion also limits nesting, node count and expanded string/key bytes
|
||||
(16 MiB), including repeated references to the same Lua string. Structural commands
|
||||
are limited to 1,024 operations and 16 MiB of marshaled payload per callback.
|
||||
The Player already runs separately from the Editor; only trusted local game projects
|
||||
should be opened and built. C++ gameplay is native code and is not sandboxed.
|
||||
|
||||
Schema extraction evaluates entry scripts in the bounded VM but does not create a
|
||||
world or invoke lifecycle callbacks. Keep top-level code declarative: calling runtime
|
||||
operations there is an error. Metadata supports the same fields, constraints and
|
||||
declarative [migration rules](api.md#editor-data-migrations) as C++ schemas. Runtime
|
||||
loading does not migrate saved data automatically.
|
||||
|
||||
## Edit, reload, and export
|
||||
|
||||
The Editor command palette exposes:
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `faset_lua_refresh` | Build if needed, extract schemas, refresh Inspector metadata |
|
||||
| `faset_lua_reload` | Request a Lua reload in a development Player |
|
||||
| `faset_lua_setup` | Install Faset LuaLS declarations/configuration |
|
||||
| `faset_script_open` | Open a script in an external editor |
|
||||
|
||||
The external-editor default is `zed`. Set `editor.script_editor` in
|
||||
`project.faset.json` to an argument array such as `["code", "--goto", "{file}"]`,
|
||||
or pass an `editor` argument array to `faset_script_open`. Exact `{file}` and
|
||||
`{project}` arguments are substituted; a missing file argument is appended. The
|
||||
command launches the executable directly, without a shell. The Assets panel lists
|
||||
Lua sources under `Scripts` and provides **Open Script**.
|
||||
|
||||
Development Play watches Lua changes. The Player's `--watch-lua` option enables this
|
||||
for direct development runs. A candidate source generation is loaded and validated
|
||||
before replacement; an invalid candidate leaves the preceding generation running.
|
||||
Successful reload **restarts the scene**, invalidates old handles, and resets all
|
||||
script state. This is not state-preserving hot swapping. C++ source changes still
|
||||
require a rebuild and a new Player process.
|
||||
|
||||
Export captures the declared entry list and Lua modules with the game. The exported
|
||||
Player runs without the Editor or a separate Lua installation; development watching
|
||||
is not enabled by ordinary exported-game launch. Exported Lua remains readable source,
|
||||
not encrypted code. A C++-only project continues to export without the Lua VM.
|
||||
|
||||
## Zed and LuaLS
|
||||
|
||||
Run `faset_lua_setup`. It copies annotation-only declarations to
|
||||
`.faset/lua/faset.lua` and creates `.luarc.json` **only if it does not already exist**.
|
||||
For an existing LuaLS configuration, merge these settings yourself:
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime.version": "Lua 5.4",
|
||||
"runtime.path": ["Scripts/?.lua", "Scripts/?/init.lua"],
|
||||
"workspace.library": [".faset/lua"],
|
||||
"workspace.checkThirdParty": false,
|
||||
"diagnostics.globals": ["faset"]
|
||||
}
|
||||
```
|
||||
|
||||
Use an editor with LuaLS integration and open the project directory. Annotations
|
||||
describe the Faset API for completion and diagnostics; they are not runtime code and
|
||||
must not be `require`d. The engine does not embed an LSP client, code editor, or a
|
||||
breakpoint debugger. Player logs/tracebacks are the first debugging surface.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Lua module validation
|
||||
|
||||
Local implementation checks, 2026-09-18. These results supplement, not replace,
|
||||
the earlier MVP acceptance record. Toolchain: Linux x86-64, GCC 13.3, CMake 4.4.3,
|
||||
Ninja 1.13.2; pinned Lua 5.4.9.
|
||||
|
||||
## Observed results
|
||||
|
||||
| Configuration | Result |
|
||||
|---|---|
|
||||
| Lua enabled, renderer/editor UI disabled | 20/20 CTest tests passed |
|
||||
| Lua disabled, renderer/editor UI disabled | 18/18 CTest tests passed |
|
||||
| AddressSanitizer + UndefinedBehaviorSanitizer, Lua suites | 3/3 tests passed |
|
||||
| Renderer-linked native Player and SchemaExporter | Built successfully; CPU Lua CLI contracts passed |
|
||||
| Lua-only project without project C++ files | Empty native adapter built; sample validated; exactly two Lua schemas exported |
|
||||
| Native Editor and Editor UI library | Compiled and linked; Editor `--help` ran |
|
||||
| Manual | MkDocs strict build passed |
|
||||
|
||||
The Lua tests exercise lifecycle ordering, per-instance fields/state, VM ownership,
|
||||
stale/cross-world handles, deferred structural operations, native physics contacts,
|
||||
`require`, invalid schemas, CPU/memory limits and the shipped example scene. Additional
|
||||
safety cases cover deep/cyclic JSON, repeated-string/key expansion, structural queue
|
||||
limits, protected metatables, repeated OOM and reclamation of a failing instance.
|
||||
|
||||
BuildService tests exercise source snapshots, fingerprints, changes during a build,
|
||||
Lua-only projects, export contents/notices, and switching back to Lua-free games.
|
||||
Their native build/export fixture is a stand-in, not a graphical Player execution.
|
||||
|
||||
## Reproduce the CPU suite
|
||||
|
||||
```sh
|
||||
cmake -S . -B build/lua-check -G Ninja -DCMAKE_BUILD_TYPE=Debug \
|
||||
-DFASET_ENABLE_LUA=ON -DFASET_BUILD_RENDERER=OFF -DFASET_BUILD_EDITOR=OFF
|
||||
cmake --build build/lua-check --parallel
|
||||
ctest --test-dir build/lua-check --output-on-failure
|
||||
```
|
||||
|
||||
Use a separate build directory with `-DFASET_ENABLE_LUA=OFF` for the optional-module
|
||||
check. For sanitizers, configure with `-DFASET_SANITIZERS=ON`, build the
|
||||
`faset_lua_tests`, `faset_lua_safety_tests`, and `faset_schema_exporter` targets, then
|
||||
run `ctest --test-dir <build> --output-on-failure -R '^lua_'`.
|
||||
|
||||
## Not verified here
|
||||
|
||||
- Windows compilation or execution of the new module.
|
||||
- Graphical/window interaction and real-time Lua reload in a running rendered game.
|
||||
`lua_player_reload` is provided as a GPU-labelled integration test for an equipped host.
|
||||
- A complete real Release export launched on a separate machine.
|
||||
- LeakSanitizer: this execution environment uses tracing incompatible with its
|
||||
process inspection, so sanitizer runs used `ASAN_OPTIONS=detect_leaks=0` and
|
||||
`UBSAN_OPTIONS=halt_on_error=1`. Address/undefined-behavior checks stayed enabled.
|
||||
|
||||
The renderer-linked CPU checks used the existing Vulkan loader, repo-pinned Vulkan
|
||||
headers and cached Slang, with SDL X11/Wayland disabled. No system graphics packages
|
||||
were installed. This proves linkage and CPU validation, not graphics compatibility.
|
||||
Reference in New Issue
Block a user