# 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.