Embedding Kalem
Kalem knows no town. A host, the game, runs worlds and gives Kalem what it needs: host types, events, contracts and natives. In Caria the host is the Go server (Play today, the live world later); on screen, Play's view in Sakin Editor hosts client scripts through WebAssembly.
What the host keeps
The world's lifecycle, field values, how saves are laid out and stored, the network, the database, the events coming in, and the game's own services. Kalem reads fields from the host and, when a transaction ends, hands it the writes as effects.
The catalog
The host describes itself in a JSON catalog; the compiler and the language server check
scripts against it. Caria's is Sakin Editor's kalem/hosts.json with the project's .event
documents:
{
"abi": 1,
"hosts": [
{"name": "Building", "side": "server", "attach": ["building"], "doc": "…"},
{"name": "Light", "side": "client", "attach": ["part"], "doc": "…",
"entry": {"name": "apply", "params": [{"name": "elapsedSeconds", "type": "float"}],
"result": "color", "effect": "pure"},
"properties": [{"name": "reach", "type": "float"}]}
],
"natives": [
{"name": "weekday", "side": "server", "result": "int", "effect": "read", "doc": "…"}
],
"events": []
}
- Host types: name, side and what they attach to (Host types).
A client type declares its entry function and its effect, and the properties the host reads
from every binding (a
Light'sreach); every script of that type declares them, and the compiler refuses one that is missing or of another type. - Events: id, scope (
selforcity), payload fields in the JSON types of Type mapping, and producer. - Natives: signatures in Kalem types, side, effect and cost.
Host natives
As in Papyrus, a host native has two halves: the compiler learns its signature from the catalog, and the host registers its implementation when the world runs.
- In bytecode every form is named by its signature (
weekday(),distance(int, int)) and listed in the script'shostNativesas compiled. The VM compares the list with the host's registrations when it binds the script, and refuses the script when a form is not registered or its result type differs. - A native that returns a value of another type fails the transaction (
host). - Natives must be short and bounded: the VM cannot interrupt a long native call.
- A native never hands back a live, mutable object of the host, only values; the world changes
only through the transaction and the script's
writes. - An expected outcome of the game ("no route") is a return value, not an error. Natives that make a task wait (next version) return an action id for ownership and cancelling.
Caria's calendar is made of such natives (year month day weekday season); the core itself has
only now() and clock().
Contracts
An object's contract is its fields (type, min/max, default) and its signals. The compiler
checks the fields a script declares; the host checks the object's contract when it binds the
script.
Publishing
A host can have an object publish events from its fields: a rule names the event, the field that
sends it (when), and which fields fill its payload. When a transaction really changes when
and is applied, the event joins the same moment's queue, after object.stateChanged, with the
fields' new values; a payload field that is not named takes its default. Rules are checked
against the catalog and the contract when the world is set up. Caria's world can send the
town's events this way, such as city.powerChanged from power.on.
Time
The world's clock counts whole minutes. A host whose engine steps in less than a minute defines, when it is bound, which minute its events fall in.
The C interface
kalem-capi (include/kalem.h) is what a host in another language calls: Go through cgo, and
the native game. A world lives on one thread: every call on it, and every native it calls back,
happens there. Every call answers with JSON; strings Kalem returns are freed with
kalem_string_free.
| Function | |
|---|---|
kalem_world_new(setup, native, context, &error) | compiles the scripts and starts the world; setup is JSON: the catalog, the scripts' sources, the objects with their contract fields and bindings, the seed and the start minute |
kalem_world_advance(world, to) | moves the clock on to minute to and answers the report |
kalem_world_deliver(world, event) | brings an event {"id", "object", "payload", "engine"} |
kalem_world_report(world) | what happened since the last report: the trace, failures, every field's value, the instances |
kalem_world_checkpoint(world) | the world written down at a moment's boundary, {"kalem", "fields"} (Saves) |
kalem_world_load(setup, saved, …) | goes on from a save made with the same definitions; no handler runs |
kalem_world_migrate(setup, saved, …, &report, &error) | opens a save with new definitions as one migration, all or nothing, reporting what was kept, changed and dropped |
kalem_world_debugged, kalem_world_debug, kalem_world_breakpoints | a debugger, called back at every pause with the pause as JSON; it answers how to go on (continue, over, into, out) and may block while the world waits |
kalem_world_free, kalem_string_free |
A host native is one callback, native(context, form, args, minute): the form ("weekday()")
and its arguments as JSON; it answers with its result as JSON, or NULL to fail the transaction.
On screen
The client host runs the visual effects every frame:
- Colours are the values of Cocos' material colour (
mainColor); the VM converts no colour space. Channels come in as 0–255 and are divided by 255; results are multiplied by 255, and Cocos clamps and rounds. The host does not touch the alpha. - The host turns milliseconds into
elapsedSecondsat this border. - Signal updates are checked by run, sequence and revision; an old run or sequence is refused.
- The target is 100 parts × 4 effects × 60 frames, 24,000
applycalls a second. A program is shared per script; nothing is allocated on the hot path; each result is written to its material once a frame; in the native game every effect is computed in one batched call per frame. In the browser 24,000 calls take about 20 ms; the cost of crossing between JavaScript and native code is measured on real Android and iOS devices.