Talebench

Reference

Package interface 1.2 (what interface in module.json names) · Talebench's module interface 3.5

Messages

Every message is JSON text. The engine sends a point; while answering, the add-on may make host calls and proposals; its answer is one JSON object.

points
prepare

once per content key in the sandbox (tb_prepare, the SDK sends it): read and keep what it needs of its content

on

a story moment, moment (start, start_at, after_command, loaded, rested, arriving, passage_entered, passage_shown, exit_taken …) and the passageId

take

an act of a kind it owns (act: its effect, debug command or command)

ask

a question about something it knows (question), its state given

contribute

its part of shown text (kind: text, spans) or of a page's reading (kind: page, text)

formula

a formula function it answers (name, args), its state given

view

a screen of its own (surface: page or step, name, data), its state given

migrate

its content and state of an earlier version (from, content, state) brought to its own, pure, answering { content, state } (either left out: kept); only when its file's points name it (A package's migrations, L-197 i10)

check

its own checks of the game's content (no game played): it answers { problems: [{ code, severity, pointer, message: {en, de} }] }, each code one it owns as check:<code> (A package's own checks, L-197 i16)

host calls
content

its own content (Talebench's Collections: content.collections)

content_key

a key that changes with its content (read and parse it again only then)

story

the game's whole content (only in check)

state

its state slot, GameState.modules.<id>

meta

its meta slot, MetaState.modules.<id>

value

a game value by its key

eval

a condition in the story's context, without draws (formula)

draw

a number from the engine's seeded draws

proposals
set_state

its whole state slot (state)

effects

the format's effects, applied as the core's (effects)

set_meta

a JSON merge patch on its meta slot (patch), journaled

emit

an event of a kind it owns (event; any other refused, module.refused)

warn

a warning (code, message); a code it owns as warning:<code> kept, any other as module.<code>

answers
again

on: ask once more in this round (what it changed is read by its next answer)

flow

on: took when it took the moment

error

take: refused, its code and message

took

take: the act was its

done

take: the act is finished

answer

ask: yes

spans

contribute (text): the spans with its marks

text

contribute (page): the reading with its part

value

formula: a number, yes/no or text

parts

view: the view's parts (checked by the engine: A package's screens)

problems

check: the problems it names, each { code, severity, pointer, message: {en, de} }

stopped

given by the sandbox when a call stops (a package that answers it is taken as stopped): what the call proposed is dropped, module.stopped

module.json

FieldWhat it holdsChecked

id

<publisher>.<name> (Talebench's own: talebench.<registry id>, e.g. talebench.collections)

lowercase letters, digits and -, one dot

name, description

{ "en": …, "de": … }

both languages, not empty

version

semver major.minor.patch

three numbers

interface

the package interface it needs, major.minor (1.0)

the major the engine carries, a minor not above the engine's

formatVersions

the Talebench format versions its content and state are written for, [from, to]

the engine's format version within

capabilities

points (the points it answers: on, take, ask, contribute, formula, …), owns (the kinds it carries out and emits: effect:, debug:, command:, step:, event:, warning:, and the codes of its checks, check:), changesCoreValues (yes/no: proposes effects on the core's values), page (yes/no: a sandboxed page, with page naming its file: A package's sandboxed page)

known points and kind prefixes; page exactly when the file is named

logic

{ "file": "logic.wasm", "bytes": …, "sha256": … }

the file there, its size and hash the same, at most 2 MB, compiling in the sandbox

test

{ "story": …, "walkthrough": … }: its test story and walkthrough by path (L-197 d4)

both files there; the story played twice on upload

content

its settings: { "schema": …, "fields": { "<path>": { "label", "help", "view" } } } (L-197 i9)

the schema subset; every field labelled in both languages

state

its part of a save: { "schema": … } (L-197 i13)

the schema subset

book

its stand-ins in the book: { "steps": { "<name>": { en, de } }, "effects": { … } } (L-197 i14)

each text in both languages

assets

its pictures: { "<name>": { "file", "contentType", "bytes", "sha256" } } (L-197 i15)

each file there, a PNG, JPEG or WebP, its size and hash as named, at most 512 KB each and 2 MB together

Points: on, take, ask, contribute, formula, view, migrate, check. Kinds an add-on may own: effect:<name>, debug:<name>, command:<name>, step:<name>, event:<name>, warning:<name>, check:<name>.

Settings schema

The schema a package describes its content with (L-197 i9; docs/architecture_spec.md, "A package's own content and its form"): a subset of JSON Schema that the engine checks and the editor draws a form from. object (properties, required), string (maxLength, enum), number and integer (minimum, maximum), boolean, array (items, maxItems); default, title and description on any.

Screen parts

text, heading, value, progress, row, column, list, picture, button, found, description, entries, frame

Codes

An add-on file is refused with:

package.asset, package.format, package.function, package.id, package.interface, package.logic, package.logic_changed, package.logic_size, package.manifest, package.migration, package.no_logic, package.no_manifest, package.no_test, package.not_a_zip, package.not_deterministic, package.not_registered, package.owns, package.owns_differ, package.page, package.point, package.schema, package.talebench, package.test_story, package.test_unfinished, package.test_unused, package.test_warned, package.texts, package.too_large, package.version

While a game plays, the engine says:

  • replay.packages:

    a replay naming packages run where no sandbox is, or its package file refused (L-197 i6)

  • engine.module_newer:

    content that records a module (moduleVersions) at a newer major version than the engine carries, both versions named (Versions and dependencies)

  • module.view:

    a module's screen asked for (Engine::module_view) that it does not have, or a view with a part the player may not draw (unknown, too deep or wide, a picture not in the release) (A package's screens)

  • save.module_newer:

    a save that names a module (GameState.moduleVersions) at a newer major version than the engine carries, both versions named

  • save.module_migrate:

    a save of an older version of a package whose migration fails (stopped, or answering neither content nor state): refused rather than misread (L-197 i10)

  • module.refused:

    a module package refused what it was asked to carry out; its own code and words in the message (The interface for packages, L-197)

  • module.stopped:

    a module package's call stopped (a trap, its step budget or its memory used up): what it proposed in that call is dropped and the game goes on as without it; the package, the point and why in the message — where the engine records warnings (its moments, acts and contributions; a stopped question or formula answers nothing) (The interface for packages)

  • package.content:

    A workspace's package's own content (modules.<id>) does not fit the schema its module.json describes, or lacks a required field: at the field's pointer (A package's own content, L-197 i9)

Talebench's module interface

Talebench's own modules (battles, dungeons, collections …) use these hooks inside the engine; add-ons reach the same moments through package interface 1's messages above. INTERFACE =

3.5 since 2026-11-11 (3.5: by_hand, whether a person decides for a side, the party's or in a match both, L-205a; 3.4: migrate_state, a package's state brought forward as a save loads, L-197 i10; 3.3: view, a module's screen as data, L-197 v1; 3.1: the attack command's boost places, boost, P8-86; 3.2: the question BlowEstimate, what a set-up deals and takes against a foe for the VS screen, P8-95), interface 2 since 2026-11-05; before it, 1.1 added the story modules' hooks for Collections, 1.2 a step and a screen of a module's own for Shops, 1.3 a part of the view, a passage kept open, a way back, a battle's end and the test runs' tries for Camp, 1.4 a start, a passage entered and shown, time passing and the moment for Time, 1.5 an exit target of a module's own, a journey on, a route planned and the test runs' own options for World maps, 1.6 an option offered as several and one of them taken for Decks, L-180; 2.0 gathered every hook into the points below, L-200; 2.1 the services Checks use, 2.2 a page rendered for its reading and the wait over named for its kind for Documents, 2.3 XP won, after a battle and a character's values for Levels, 2.4 a passage being entered and an encounter due for Battles, 2.5 a battle started, ended and played on, a save loaded and a rest, 2.6 the services the battle code uses through the host (B1), L-180; 3.0 took the battle's doings out of BattleHost into the battles crate, which a module adding to a fight depends on (B3, L-180; no package was built against 2.x, so none is kept beside)

HookThe engine calls it whenThe module answersWithout the module

id, def, prefix

—

its name, its registry row (version, needs, switch), the prefix of its own options' IDs

—

on

the game reaches a moment (Moment: a start or a load, a start on a place of its own, after a command, a save loaded, the party rested, a passage being entered (before its own time) and entered, an encounter due on entering, a passage shown, an exit taken, a way back, time passing, a wait resumed, a journey on, a battle opening, running, resumed, a turn starting, offered or ending, before an action, a foe's or the party's action, after an action, XP won, after a battle, a battle ended)

Flow::Took where the moment asks (it waits, it took the step, it decided the way on), else Flow::Pass

nothing happens

take

something of a kind it declared is to be carried out (Act: a command, an effect, a step, an exit's target, an offer it made, an option with its prefix, the attack's aim, a debug command, a battle started, ended or played on)

its result (Ok(true): a step that waits); None: not its own

the core's warning or engine.lacks

ask

the game asks a question (Question: may the party camp, is a command refused, does a passage stay open, does a fighter defend, who pays a skill and is it usable, is a skill no action, a foe's intent skill, is a foe or a mark known, does the battle preview, the suggested aim, the run offer, the zones, an exit of its own, a test run's way out, loop, options and commands, a named count)

an Answer of the kind asked; None: it passes

the core's own answer

migrate_state

a save recording an older version of a package loads (L-197 i10: the package's migrate)

its state slot as this version has it, or why it cannot

the slot as saved

fill

a surface is shown (Fill: the game's view, an "Every choice" run's tries)

its part, changing nothing

the core's part only

contribute

the player meets something (Contribution: options before the passage's, something instead of them, an option offered as several, a text or a page read, a route planned)

its part, and what meeting it changes (an entry revealed, a card drawn)

the core's part only

formula

a formula calls a function no core part has

its value

none (the formula's error)

view

the player asks for a screen of the module (its page from the game's menu, a step's)

its view in the parts the player draws (L-197 v1)

none: nothing is drawn

new_scratch, reconcile

a game's engine is made; a save meets content that changed

its working state; its own state made to fit (true: the position no longer fits)

none; nothing changed

action_starts, blow, blow_landed, blow_effects

an action's effects begin; a damaging blow on a target; a blow it decided landed; after a result on a target

its state for the action; the blow it decides; statuses to put on; effects to add

the damage formula decides; none

test_seen, test_never

a test run sees an event; a test run ends

what it shows covered; what of its own no run came to

nothing

Versions and the compatibility promise

An add-on names the package interface it needs (interface, major.minor); a version of it never changes once uploaded, and a newer one brings content and saves forward with migrate. Talebench keeps its own promise:

  • During the prototype (internal testing), the format may change without a migration; project files, saves and releases from that time carry no guarantee.
  • From the first version used outside the team (the compatibility baseline: format version 4, recorded on 2026-10-16, BASELINE_FORMAT_VERSION), every later version (showing the demos to visitors who only play, as in P3-62, is not such a use: nobody outside the team keeps a project file or save from it): - imports every project file any earlier version exported, and plays every earlier release and save, through the migrations above; - reads older manifest.json files of project files: fields are only added, never renamed or removed, and unknown fields are ignored, so an older file never needs its creator's version.
  • Breaking changes after the baseline are made only as a new formatVersion with a migration; refusing an older file is a bug.
  • CI keeps one stored project file per past formatVersion (from the baseline on) next to the stored manifest, save and replay, and checks that it imports, validates and plays. For the baseline they were written once from the demos (WRITE_BASELINE=1 runs the writer in packages/demos/test/demos.test.ts) and are never changed: engine/samples/v4/ holds The Ember Road's content, its release manifest and a save at its first travel offer (cargo test loads the save and travels on, and parses the manifest also with a field it does not know), engine/tests/replays/baseline-v4-ember-road.json the replay of the way there (it must reproduce its expected run; events may gain fields after the baseline, as the manifest does, so its run must hold every field of the expected one unchanged and may hold more), and engine/samples/v4/project/clockmaker/ an unpacked project file, which the API's integration test imports, with and without an unknown manifest.json field, and makes a preview release of.
  • What keeps the promise: the player's shell reads a release's content only after migrating it (migrate_manifest in the player build; saves still name the content's hash as stored), as the engine does; a replay names the format version of its content and initial state, which are migrated before it runs (its hash is checked on the stored content), and the engine refuses content of another version than its own instead of reading it as current; the meta state, a save record and a .talebench-save file carry a format version too, with their own migration steps (one stored without a version is the baseline's; one from a newer player is not shown and never overwritten); a draft restored from the history is migrated like any draft loaded; the API's FORMAT_VERSION is the engine's (format_versions()), checked by a test. A word that becomes reserved in formulas (a new keyword or function name) comes with a new format version whose migration renames keys that use it; each step's list of words is fixed in its migration, never shared with the language's current list.
  • The baseline's stored files are checked against a list of their SHA-256 sums (engine/samples/v4/SHA256SUMS, every file of the baseline listed), so a run that rewrites expected results (UPDATE_REPLAYS) cannot change them. Besides the files above they hold, all written once with WRITE_BASELINE_SAVES=1: a save at each kind of wait (choices, continue, check, battle, read, ask, a stay reply, travel, ending) with the demo content it belongs to (saves/, demos/; engine/crates/talebench-core/tests/baseline.rs loads each), a meta state and a test state from the demos' walkthroughs, a .talebench-save file and the player's stored record as testers' browsers hold them (player/, read by the player's tests), and a project file written by the real exporter (project-export/, imported by the API's integration test).
  • Saves from places that came after the baseline (still format 4) are stored the same way, once, in a dated set beside it that is never changed either: engine/samples-later/2026-10-23/ (P8-12) holds a save in a planned battle (The Sunken Choir), at a shop and at the camp (The Ember Road) and at a deck's offer (The Lantern Keeper), each with its demo's content and a SHA256SUMS list, written with WRITE_LATER_SAVES=2026-10-23 by packages/demos/test/demos.test.ts; a second set, engine/samples-later/2026-10-23b/ (P8-14), holds a save inside an open conversation level and one after a visit left from a level (returnLevels; Wintermere's talk with Oskar) and one while riding (the Ember Road's courier on Bramble), written with WRITE_LATER_SAVES=2026-10-23b. The writer refuses a set whose SHA256SUMS exists, so no run rewrites one; a set written later the same day takes a letter. A battle kind that ships after a set gets saves in its middle in the next set: the third set, engine/samples-later/2026-11-07/ (P8-22), holds dice battles with the standard's fighter fields — the throw for who starts still to come, the party's turn in round 2 and a foe's blow waiting for the defence (Dice Duel's first table) and a foe's defence laid face down (the classic tables' Masked Gambler), written with WRITE_LATER_SAVES=2026-11-07. baseline.rs loads each natively and the demos' tests load each in every engine build (playtest, release-full, and release-story where the build plays the content), all still waiting where they were saved. A later set gets a new dated directory (a second one on the same day a letter: 2026-10-23b); the sets sit outside engine/samples/, whose directories are one per format version.

The public API

A workspace's own tools read and change its projects with a key made in the workspace settings. Every route and tool is described in the API's OpenAPI file.

JSON schemas