126 lines
5.9 KiB
Markdown
126 lines
5.9 KiB
Markdown
# Addon SDK — design spec
|
|
|
|
This is the differentiating feature: **easy add-ons with guard rails.** Modders
|
|
get a friendly, well-documented API; the server stays safe from buggy or
|
|
malicious addons.
|
|
|
|
## Design goals
|
|
1. **Easy to write.** A modder writes a small Lua script + a manifest. No build
|
|
step, no engine knowledge.
|
|
2. **Impossible to hurt the host.** No filesystem, no network, no arbitrary code
|
|
against the engine, no hanging the server tick, no unbounded memory.
|
|
3. **Server-authoritative.** Addons run on the server. Their effects replicate to
|
|
clients like any other game state.
|
|
4. **Content without code where possible.** Many mods (new items, recipes,
|
|
building pieces) are pure *data* — declared in a manifest, no Lua needed.
|
|
|
|
## Why Lua (not WASM, not GDScript, not Python)
|
|
- Lua is the industry-standard embedded game-modding language. Modders already
|
|
know it (WoW, Factorio-ish, Garry's Mod, countless others).
|
|
- A Lua VM is trivially sandboxable: you build the environment table yourself, so
|
|
the addon literally cannot see anything you don't hand it.
|
|
- GDScript was rejected: it can reach engine internals — too powerful for untrusted
|
|
code.
|
|
- WASM was considered (stronger isolation, authors use Rust/AssemblyScript) but is
|
|
harder for casual modders. Keep it as a possible *advanced* lane later.
|
|
- Embedding: a Lua-in-Godot binding (e.g. `godot-luaAPI` GDExtension) or a custom
|
|
GDExtension wrapping Lua 5.4 / LuaJIT.
|
|
|
|
## Addon package layout
|
|
```
|
|
my_addon/
|
|
addon.json manifest (required)
|
|
main.lua entry script (optional — data-only addons skip it)
|
|
items/ data definitions (optional)
|
|
prefabs/ low-poly meshes/scenes the addon adds (optional, validated)
|
|
assets/ textures, icons (optional, size-capped)
|
|
```
|
|
|
|
### addon.json (manifest)
|
|
```json
|
|
{
|
|
"id": "com.paul.ironpike",
|
|
"name": "Iron Pike",
|
|
"version": "1.0.0",
|
|
"api": "1", // SDK API version it targets
|
|
"author": "Paul",
|
|
"permissions": ["items", "recipes", "events"], // explicit capability opt-in
|
|
"entry": "main.lua" // omit for data-only addons
|
|
}
|
|
```
|
|
- **Permissions are explicit.** An addon only gets the API groups it declares, and
|
|
the server can refuse/limit them. A data-only addon declares no code perms at all.
|
|
|
|
## The sandbox (how the guard rails actually work)
|
|
1. Each addon gets its **own fresh Lua VM** (isolation between addons).
|
|
2. The global environment is a **whitelist table we build** — standard-library
|
|
access is stripped: no `os`, `io`, `package`, `require`, `dofile`, `loadfile`,
|
|
`load`, `debug`. Only safe primitives (`math`, `string`, `table`, `pairs`,
|
|
`ipairs`, `tostring`, a sandboxed `print` that routes to the server log).
|
|
3. The host injects a single global, `game`, which is the **entire API surface** —
|
|
the only way an addon touches the world.
|
|
4. **Instruction/time quota:** every addon callback runs under a hook that counts
|
|
VM instructions (or wall-clock) and aborts if it exceeds the budget. An addon
|
|
that loops forever is killed, the tick is never blocked.
|
|
5. **Memory quota:** the VM's allocator is capped; exceeding it aborts the addon,
|
|
not the server.
|
|
6. **No reentry into the engine:** the `game` API functions are thin, validated
|
|
shims written in GDScript/C++. The addon can't reach the Godot API, the
|
|
filesystem, or the network.
|
|
7. **Fault isolation:** an error in an addon callback is caught, logged with the
|
|
addon id, and the addon can be disabled — the server keeps running.
|
|
|
|
## API surface (v1 sketch — `game.*`)
|
|
Events (register handlers):
|
|
```lua
|
|
game.on("player_join", function(player) ... end)
|
|
game.on("block_place", function(player, piece, pos) ... end) -- can veto
|
|
game.on("tick", function(dt) ... end) -- quota-capped
|
|
game.on("entity_death", function(entity, killer) ... end)
|
|
```
|
|
World queries (read-only, validated):
|
|
```lua
|
|
local p = game.players.get(id) -- safe player handle
|
|
local n = game.world.count_entities("tree")
|
|
local b = game.world.get_block(x, y, z) -- within loaded bounds only
|
|
```
|
|
World commands (intents the host validates & applies):
|
|
```lua
|
|
game.world.spawn("wolf", x, y, z) -- subject to limits/quotas
|
|
game.chat.broadcast("Welcome, " .. p.name)
|
|
game.inventory.give(p, "wood", 5) -- bounded
|
|
```
|
|
Content declaration (data, validated at load):
|
|
```lua
|
|
game.define_item{ id="iron_pike", name="Iron Pike", damage=42, stack=1 }
|
|
game.define_recipe{ output="iron_pike", inputs={iron=10, wood=4}, station="forge" }
|
|
```
|
|
Everything an addon can do lives under `game.`. If it's not there, the addon can't
|
|
do it — that's the guard rail.
|
|
|
|
## Limits the server enforces (configurable by the host owner)
|
|
- Max addons loaded
|
|
- Per-callback instruction/time budget
|
|
- Per-addon memory ceiling
|
|
- Rate limits on spawn/give/broadcast (so an addon can't spam)
|
|
- Asset size caps (prefab poly count, texture dimensions)
|
|
- Permission allow/deny per addon
|
|
|
|
## Threat model (what we explicitly defend against)
|
|
| Threat | Defence |
|
|
|--------|---------|
|
|
| Addon reads server files / secrets | No `io`/`os`; no filesystem in the sandbox env |
|
|
| Addon opens network connections | No socket library exposed |
|
|
| Addon hangs the server (infinite loop) | Instruction/time quota aborts the callback |
|
|
| Addon exhausts RAM | Per-VM memory cap |
|
|
| Addon crashes the server | Every callback wrapped; errors isolate & disable the addon |
|
|
| Addon escalates via another addon | Each addon = separate VM, no shared globals |
|
|
| Addon spams spawns/messages | Host rate limits on world commands |
|
|
| Malicious prefab (huge mesh) | Asset validation + poly/texture caps at load |
|
|
|
|
## A test we must pass (M3 done-criteria)
|
|
Ship a deliberately-malicious example addon in the test suite that tries to:
|
|
read a file, open a socket, loop forever, and allocate gigabytes. The server must
|
|
contain all four, log them, disable the addon, and keep running. If that passes,
|
|
the guard rails are real.
|