Expressions ship as trees
Every gate and script is parsed once at sync time. The engine evaluates a tree and ships no parser at all.
All documentation — Concepts
Start here
Concepts
Authoring for a game
Playing a story
The generated code
The plugin and sync
On this page
TaleShard has an expression language — gates on exits, conditions on requirement rows, scripts that run when a node is entered. None of it is parsed in your game.
The language is one-way: every expression in a project is authored in the editor and never composed at run time. So the sync server parses each one once into a portable tree, and the payload carries the trees. The engine ships an evaluator and no parser.
What that means at the API
Every entry point on the script runtime takes an expression id, never source text. There are three of them, and each engine spells them in its own language:
- evaluate a condition — takes an expression id, answers true or false;
- evaluate a condition and report the refusal — the same, and also hands back the reason a false answer was false;
- execute a script — takes an expression id, answers an execution result.
A field that carries a gate or a script exposes the id of its parsed tree, and that id is what you hand to the runtime. There is no overload taking a string of source, deliberately: adding one would mean shipping a parser, and a parser is a second implementation of the language that would have to agree with the first one forever.
What the evaluator carries
The value model and its coercion table, a standard library over numbers, text, lists and sets, collection macros, visit and seen counts, variable methods with a purity flag, a scope chain, the playthrough’s entity overlay, a seeded generator whose state is part of the save, evaluation budgets, and an execution trace.
All of it mirrors the browser runtime behaviour for behaviour, because the runtimes are held together by one conformance corpus: the same expressions are evaluated by the browser evaluator, the C# one and the C++ one, and asserted to produce identical values and identical refusals, and the same fixture graphs are walked by each engine and asserted to produce identical step sequences. Every one of those suites runs headlessly on every build.
The failure policy
This is worth memorising, because it is the part that surprises people:
- A gate that throws evaluates to
false, and is recorded. Never a crash, never a silenttrue. - A present-but-blank gate fails on an exit the story takes by itself. An absent gate passes. There, an author who deleted the text of a condition did not mean “always allow”. On an exit the player is offered, a blank gate is no condition and the option is available.
- A gate cannot have effects. The serialised tree carries the distinction between a builtin and a command explicitly, so a command reached in gate mode is refused rather than called. A condition cannot fire your audio.
Determinism
The seeded generator is seeded from the playthrough, never from a clock, and its state is part of the save. That is what makes a playthrough reproducible: the same project and the same seed give the same story, including anything the author left to chance. If you build a session yourself, pass a stable playthrough id as the seed — see starting a session.
What an unresolved expression means
Some expressions may arrive without a usable tree. The binding reports them, in two lists: the expression ids whose tree could not be read at all, and the condition-variable codes whose expression could not be resolved. Both are worth surfacing, because each one is a gate that will refuse at play time or a name that reads as permanently absent — and neither announces itself while the story is running.