Return shapes, units, error handling, and require style.
API conventions
Every engine service is a plain Lua table you import with require. This page explains how those tables are shaped, how results come back, how failures are reported, and which units the numbers use — read it once before diving into the individual APIs.
What a service module is
A service module is a small table of functions grouped by job (entities, transforms, audio, and so on). You import it into a local and call through that local. There are no engine globals: if you forget the require, the name is simply nil and the call fails loudly instead of silently doing something else.
local Entity = require("demi.entity")
local Transform3D = require("demi.transform3d")
local id = Entity.find("skyforge_player")
local x, y, z = Transform3D.get_position(id)
require("demi.entity") returns the entity service table. Entity.find("skyforge_player") looks up an entity by ID or name and returns its stable ID string, or nil when missing. Transform3D.get_position(id) returns three local-position scalars, or three nil values when unavailable. The local names (Entity, Transform3D) are yours to choose; the "demi.…" strings are the contract.
Vector annotations
When an API documents a vector type it uses the names Vec2 and Vec3. Those are annotations for readers and editor tooling, not Lua classes: a Vec3 is just three numbers. How you hold them depends on the call:
- Transform getters and setters pass coordinates as separate scalar arguments and returns:
local x, y, z = Transform3D.get_position(id). - Vectors embedded inside tables (spawn options, hit results, color-style fields) are fixed-length numeric arrays:
{ x, y }or{ x, y, z }. Lua arrays start at index 1, so read them asv[1],v[2],v[3].
local crate = Entity.spawn("skyforge_crate", {
position = { 4, 2, 0 }, -- Vec3 as a 3-number array
velocity = { 1, 0, 0 },
})
local px, py, pz = Transform3D.get_position(crate) -- three scalars
Entity.spawn(id, options) takes the new stable ID and an options table. Its position and velocity shorthands are dense numeric arrays of length two or three; their dimensions must agree when both are supplied. It returns true when creation was queued, or false plus an error string on failure. The entity is created at the next creation step, not synchronously. The transform getter returns separate scalars.
How results come back
| Shape | Where it is used | How to read it |
|---|---|---|
local x, y, z = … |
Transform position, rotation, scale | One scalar per coordinate. If the entity has no transform, each coordinate is nil — check one of them before doing arithmetic. |
{ x, y } / { x, y, z } |
Dense coordinate inputs, such as spawn position/velocity and authored sizes | Numeric arrays indexed from 1. Follow the particular function's input contract. |
{x = …, y = …, z = …} |
Named vectors returned inside ray, contact and other result tables | Read fields such as hit.point.x. Do not assume these outputs use the dense-array shape required by some inputs. |
value, nil or nil, err |
Data.load, Data.parse_yaml |
Check the first result. Success is value, nil. Failure is nil plus an error table (shape below). |
nil |
Missing entity, unknown HUD node, missed raycast | A single nil means “nothing found”. Guard with if hit then … end before reading fields. |
{} (empty table) |
Bulk queries, children lists, path results from NavigationGrid |
An empty array means “the query ran and found nothing”, not an error. Iterate with ipairs. |
Two pathfinding calls illustrate the split between nil and empty tables. Grid.path returns a path table on success and a single nil (plus a diagnostic you can log) when no path exists. nav:path always returns a table of waypoints — empty when nothing was found — and a diagnostic. Treat nil as “no result object”, and an empty table as “a result with zero items”.
local Grid = require("demi.grid")
local nav = require("demi.navigation").default_grid()
local gridPath = Grid.path(0, 0, 5, 5)
-- gridPath: array of {x, y} waypoints, or nil when unreachable (a diagnostic code is returned)
local navPath = nav:path(0, 0, 5, 5)
-- navPath: always a table of waypoints (possibly empty) plus a diagnostic
Grid.path(start_x, start_y, goal_x, goal_y) takes four tile coordinates and returns either an array of waypoints or nil plus a diagnostic code. nav:path(start_x, start_y, goal_x, goal_y, diagonal?) returns a (possibly empty) waypoint array and a diagnostic string you can pass to Debug.log.
Error shapes
Data-loading and YAML-parsing failures use nil, DataError. This is not a universal error convention: many entity/network operations return booleans, while HTTP request creation returns a handle or nil, error_string. Poll an HTTP handle's response for transport/HTTP errors. Use each service's documented return shape. A data error looks like:
local Data = require("demi.data")
local value, err = Data.load("asset://data/skyforge_enemies")
if err then
-- err.code : short machine string, e.g. "not_found" or "schema_mismatch"
-- err.message : human-readable sentence
-- err.path : JSON pointer into the document, or the URI
end
Data.load(uri) returns value, nil on success. On failure it returns nil and an error table with three string fields: code (stable, safe to branch on), message (safe to show or log), and path (where in the document the problem is, as a JSON pointer such as /stats/0/hp, or the requested URI when the whole document is missing).
JSON null
JSON has a real null value that is different from “field missing”. Lua only has nil, which also means “table key absent”, so DemiEngine exposes a sentinel instead:
local Data = require("demi.data")
local value = Data.load("asset://data/skyforge_enemies")
print(Data.kind(value.nickname)) -- "null" when the field is JSON null
print(Data.is_null(value.nickname)) -- true
local written = { nickname = Data.null }
Data.null is the single sentinel value for JSON null — a field of the Data table, not a function. Data.kind(value) returns "array", "object", or "null", and nil for scalars (numbers, strings, booleans) or missing values. Data.is_null(value) returns true only for the sentinel. Assign Data.null when you want to write a real null back out.
Units
Transform2Drotation andTransform3DEuler rotation: radians.- Angular velocity on rigidbodies: radians per second.
- Camera field of view: degrees.
- All
Timevalues (delta_time,fixed_time,time): seconds. - HUD positions and sizes: canvas units from the HUD document
canvas_size, not raw screen pixels.
Module map
Group services by the job they do. Names with dots are single modules — require("demi.physics.rigidbody3d") is one require for one table.
| Module | Use it for |
|---|---|
demi.application | Window mode, FPS cap, mouse capture and visibility, safe area, DPI, UI scale, orientation, keyboard, clipboard, focus/suspend state, user and cache paths, permissions |
demi.entity | Finding, creating, spawning, cloning, destroying entities; components; queries; parenting — see Entity API |
demi.scene / demi.prefab | Scene prepare/activate and prefab instances — see Scene & prefab API |
demi.input | Action queries, contexts, rebinding, touch, gamepads |
demi.transform2d / demi.transform3d | Position, rotation, scale; 3D directions and look_at — see Transform API |
demi.sprite2d / demi.tilemap2d | Sprite draw state and animation; tile reads and overrides |
demi.animation | State machine parameters, clips, layers, root motion — see State machines |
demi.camera3d | Screen rays and world/screen conversion |
demi.hud | UI nodes, text, variables, tweens, virtualization — see HUD API |
demi.events | Subscribing and emitting events — see Events & timers |
demi.data / demi.save | Game data documents and save slots — see Data & save API |
demi.assets | Async load/unload of asset:// resources — see Assets, time & timers |
demi.time / demi.timer | Frame time, pause, time scale; delays and repeats |
demi.debug / demi.profile | On-screen debug lines; scoped performance timers — see Math & debug |
demi.text / demi.regex | Grapheme-safe text helpers; ECMAScript pattern matching |
demi.grid / demi.navigation | Isometric placement; 2D pathfinding |
demi.video / demi.cutscene | Video playback and cutscene sequencing |
demi.test | E2E helpers — see E2E tests |
demi.network | Low-level transport — see Sessions & contracts |
| Namespaced module | Use it for |
|---|---|
demi.physics.rigidbody2d / demi.physics.rigidbody3d | Velocities, forces, impulses, sleep, kinematic movement |
demi.physics.query2d / demi.physics.query3d | Raycasts, shape casts, overlap checks |
demi.physics.character_controller3d | Character velocity, jump, grounded state |
demi.physics.destruction3d | Impacts, part damage, checkpoints |
demi.network.session | Host-authoritative multiplayer sessions |
demi.network.http | Async HTTP/HTTPS calls — see HTTP & TLS |
demi.network.tls.client / demi.network.tls.server | Verified TLS sockets |
demi.network.crypto | Random tokens, password hashing, constant-time compare |
demi.audio / demi.audio.source | Buses, snapshots, one-shots; entity-attached playback — see Audio mixing |
demi.math.vector2 / demi.math.vector3 / demi.math.scalar / demi.math.random | Vector math, easing, seeded randomness |
demi.mesh.procedural / demi.mesh.deformation | Procedural meshes; deformation |
demi.voxel.world | Chunked voxel sections and meshes |
Next steps
- Script lifecycle — how a Lua module becomes a running instance.
- Entity API — finding and spawning the objects a Skyforge script drives.
- Data schemas — declaring the shape of loaded game data.