Skip to main content

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's reach); every script of that type declares them, and the compiler refuses one that is missing or of another type.
  • Events: id, scope (self or city), 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's hostNatives as 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_breakpointsa 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 elapsedSeconds at 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 apply calls 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.