LSDE Dialog Engine — Full Guide [English] (plain text, auto-generated) ============================================================ Concatenates all guide sections for LLM consumption. Source: lsde-ts/docs/guide/*.md ============================================================ # What is LSDEDE? **LSDE** (LS Dialog Editor) is a free tool for game and software developers that combines visual dialogue graph editing, AI-powered translation, voice generation, i18n code integration, and project diagnostics. It exports dialogue graphs as blueprints (JSON, XML, YAML, or CSV) containing scenes, blocks, connections, dictionaries, and action signatures. More info: [lepasoft.com/en/software/ls-dialog-editor](https://lepasoft.com/en/software/ls-dialog-editor). **LSDEDE** (LSDE Dialog Engine) is the multi-runtime engine that loads and executes these blueprints. It is available in multiple languages for native integration into any game engine or framework. ## Available Runtimes | Runtime | Language | Target | Source | |---------|----------|--------|--------| | **TypeScript** | TypeScript / JavaScript | Reference implementation | [lsde-ts](https://github.com/jonlepage/LS-Dialog-Editor-Engine/tree/master/lsde-ts) | | **C#** | C# (.NET Standard 2.1) | Unity, Godot Mono, .NET | [lsde-csharp](https://github.com/jonlepage/LS-Dialog-Editor-Engine/tree/master/lsde-csharp) | | **C++** | C++17 | Unreal Engine, custom engines | [lsde-cpp](https://github.com/jonlepage/LS-Dialog-Editor-Engine/tree/master/lsde-cpp) | | **GDScript** | GDScript | Godot 4 | [lsde-gdscript](https://github.com/jonlepage/LS-Dialog-Editor-Engine/tree/master/lsde-gdscript) | All runtimes share the same blueprint format and pass a common cross-language test suite (42 test cases). ## Architecture Every runtime follows the same **callback-driven graph dispatcher** pattern: 1. **Blueprint** — A file exported from LSDE (JSON, XML, or YAML), containing scenes, blocks and connections. 2. **Engine** — Validates the blueprint, builds the internal graph and dispatches blocks to registered handlers. 3. **Handlers** — Functions that react to each block type (dialog, choice, condition, action). 4. **Host Application** — Conditions, actions, and character resolution are implemented by handler callbacks. ``` Blueprint │ ▼ Engine ◄── next() ──┐ │ │ dispatch │ │ │ ▼ │ Handlers ────────────┘ ``` ## Design Principles - **Zero-dependency** — No runtime dependencies in any language. - **Framework-agnostic** — Works with any game engine or UI framework. - **Callback-driven** — No internal render loop. The host application calls `next()` when ready. - **Two-tier handlers** — Global (engine-level) and scene-level handlers with `preventGlobalHandler()`. - **Cross-language conformance** — All runtimes produce identical output for the same blueprint. ================================================================================ # Getting Started ## Installation ## Minimal Usage The engine is a graph traversal machine — it dispatches blocks to registered handlers, which give them meaning. Without handlers, the engine has no output. > TIP: The engine consumes a `BlueprintExport` object, not a file. You can load your blueprint from JSON, XML, or YAML using any parser suited to your platform. See [Parsing & Import](./parsing) for recommendations. ## Blueprint Validation `engine.init()` returns a [diagnostic report](/api-ref/interfaces/DiagnosticReport) with errors, warnings, and stats. The `check` option cross-validates against the host application's capabilities: ================================================================================ # Blueprints & Scenes ## Blueprint Structure A `BlueprintExport` is the JSON file exported from the [LSDE](https://lepasoft.com/en/software/ls-dialog-editor "Lepasoft Dialog Editor") editor. It contains all the data the engine needs. ## Scenes A scene is a self-contained dialogue sequence — a conversation, a cutscene, a tutorial prompt, a shop interaction. In a game, scenes are typically triggered by script events: the player talks to an NPC, enters a zone, or picks up an item. Each scene has its own entry block, its own flow, and its own state. Multiple scenes can run in parallel (e.g. a main dialogue and a tutorial overlay). Scenes are defined by the [`BlueprintScene`](/api-ref/interfaces/BlueprintScene) interface: ## Connections Connections are the wires between blocks — they define which block leads to which. In the editor, you draw them visually; in the export, they become a flat list of source → target links defined by the [`BlueprintConnection`](/api-ref/interfaces/BlueprintConnection) interface: You won't typically need to inspect connections directly — the engine handles routing internally. They are however exposed in [`onValidateNextBlock`](/api-ref/classes/DialogueEngine#onvalidatenextblock) if needed. ## Dictionaries Dictionaries describe the registers of your game — switches, variables, inventory. The developer declares them in the [LSDE](https://lepasoft.com/en/software/ls-dialog-editor "Lepasoft Dialog Editor") editor to expose available game variables to the narrative designer. At runtime, the developer maps each dictionary to the corresponding system in their game. [`Conditions`](/api-ref/interfaces/ExportCondition) and [`onResolveCondition`](/api-ref/classes/DialogueEngine#onresolvecondition) use these keys to evaluate game state. Defined by the [`Dictionary`](/api-ref/interfaces/Dictionary) interface: ## Action Signatures Signatures describe the action types available in your game — `set_flag`, `play_sound`, `give_item`. The developer declares them in the [LSDE](https://lepasoft.com/en/software/ls-dialog-editor "Lepasoft Dialog Editor") editor so that narrative designers can compose action sequences with typed parameters. At runtime, the signature `id` is what the developer maps to their own systems. Defined by the [`ActionSignature`](/api-ref/interfaces/ActionSignature) interface: ================================================================================ # Block Types Blocks are the building blocks of a dialogue scene — each node in the editor graph is a block. The engine routes the flow from block to block and calls the matching handler for each type. There are 5 types: **Dialog**, **Choice**, **Condition**, **Action**, and **Note**. The first four are content blocks with a dedicated handler (`onDialog`, `onChoice`, `onCondition`, `onAction`) — all four are **required** and validated when `start()` is called. Note blocks are skipped automatically. Handlers come in two tiers: **global handlers** (registered on the engine) cover all scenes and are sufficient for most games. **Scene handlers** (registered on a [`SceneHandle`](/api-ref/interfaces/SceneHandle)) can supplement or override globals for a specific scene. See [Handlers](/guide/handlers) for details. ## DIALOG A dialog block represents a line of speech — a character talking, a narrator, on-screen text. The engine resolves the speaking character via the `onResolveCharacter` callback and exposes it as `context.character`. A typical dialog handler creates a text instance in the game (textbox, bubble, subtitle…), waits for the player or an animation to finish, then calls `next()` to advance the engine. The optional cleanup function lets you clean up side effects when the engine moves to the next block. When the narrative designer assigns a dedicated output per character ([`portPerCharacter`](/api-ref/interfaces/NativeProperties#portpercharacter)), the handler must call `resolveCharacterPort()` to tell the engine which path to follow on `next()`. ## CHOICE A choice block represents a branching point where the player picks a response — a dialogue menu, a list of options. `context.choices` contains all available options. When [`onResolveCondition()`](/guide/choice-visibility) is configured, each option is tagged `visible: true | false` — the handler filters and displays whichever it wants. After the player interacts, `selectChoice(uuid)` tells the engine which path to follow, then `next()` advances the flow. See [Choice Visibility](/guide/choice-visibility) for the full opt-in tagging system. ## CONDITION A condition block is an invisible switch — it evaluates game state and silently sends the flow down one or more paths without the player seeing it. Conditions are grouped in a 2D array: each group is a "case" evaluated as an AND/OR chain. **When `onResolveCondition` is installed**, the engine pre-evaluates all groups before calling `onCondition`. Each group in `context.conditionGroups` has a `result` (true/false) and a `portIndex`. The engine auto-resolves the routing — `onCondition` is optional and serves as a logging/override hook. **Routing modes:** - **Switch mode** (default): first matching group index routes the flow. `-1` (no match) follows the default port. - **Dispatcher mode** (`enableDispatcher: true`): all matching group indices fire as independent async tracks, default port is the main continuation. `context.resolve()` accepts `boolean` (legacy), `number` (switch), or `number[]` (dispatcher). ## ACTION An action block fires side effects in the game — give an item, play a sound, set a flag. Each action references an `actionId` that the developer maps to their own systems. The handler executes the action list then calls `context.resolve()` to follow the "then" port, or `context.reject(error)` to follow the "catch" port (falls back to "then" if no "catch" connection exists). ## NOTE A note block is a sticky note for the narrative designer — comments, reminders, context. It is automatically skipped during traversal. While it is technically possible to intercept a note block via [`onBeforeBlock`](/guide/lifecycle), this is not recommended — the action block should cover all your side-effect needs. ## Common Properties All blocks share these base fields ([`BlueprintBlockBase`](/api-ref/interfaces/BlueprintBlockBase)): | Field | Type | Description | |-------|------|-------------| | [`uuid`](/api-ref/interfaces/BlueprintBlockBase#uuid) | `string` | Unique identifier | | [`type`](/api-ref/interfaces/BlueprintBlockBase#type) | `BlockType` | Discriminant type | | [`label`](/api-ref/interfaces/BlueprintBlockBase#label) | `string?` | Human-readable name | | [`parentLabels`](/api-ref/interfaces/BlueprintBlockBase#parentlabels) | `string[]?` | Parent folder hierarchy from the editor | | [`properties`](/api-ref/interfaces/BlueprintBlockBase#properties) | `BlockProperty[]` | Key-value properties | | [`userProperties`](/api-ref/interfaces/BlueprintBlockBase#userproperties) | `Record?` | Free-form user properties | | [`nativeProperties`](/api-ref/interfaces/BlueprintBlockBase#nativeproperties) | `NativeProperties?` | Execution properties | | [`metadata`](/api-ref/interfaces/BlueprintBlockBase#metadata) | `BlockMetadata?` | Display metadata (characters, tags, color) | | [`isStartBlock`](/api-ref/interfaces/BlueprintBlockBase#isstartblock) | `boolean?` | Marks the entry block | ### NativeProperties | Field | Type | Description | |-------|------|-------------| | [`isAsync`](/api-ref/interfaces/NativeProperties#isasync) | `boolean?` | Execute on a parallel async track | | [`delay`](/api-ref/interfaces/NativeProperties#delay) | `number?` | Delay before execution (consumed by `onBeforeBlock`) | | [`timeout`](/api-ref/interfaces/NativeProperties#timeout) | `number?` | Execution timeout | | [`portPerCharacter`](/api-ref/interfaces/NativeProperties#portpercharacter) | `boolean?` | One output port per character in metadata | | [`skipIfMissingActor`](/api-ref/interfaces/NativeProperties#skipifmissingactor) | `boolean?` | Skip block if the assigned actor is missing | | [`debug`](/api-ref/interfaces/NativeProperties#debug) | `boolean?` | Debug flag for the editor | | [`waitForBlocks`](/api-ref/interfaces/NativeProperties#waitforblocks) | `string[]?` | Block UUIDs that must be visited before this block can progress | | [`waitInput`](/api-ref/interfaces/NativeProperties#waitinput) | `boolean?` | Passive flag for explicit player input control | | [`enableDispatcher`](/api-ref/interfaces/NativeProperties#enabledispatcher) | `boolean?` | Condition block: all matching groups fire as async tracks | ================================================================================ # Choice Visibility ## Overview When a CHOICE block is dispatched, `context.choices` always contains **all** choices defined in the blueprint — none are pre-filtered. The engine never removes choices from the array. If visibility filtering is needed (e.g., hiding choices based on game state or previous selections), the engine provides an **opt-in tagging** system. A condition resolver is installed once, and the engine tags each choice with `visible: true | false` before the `onChoice` handler sees it. ## Setup Register a condition resolver on the engine — once, before starting any scene: When installed, the engine evaluates each choice's `visibilityConditions` **before** calling `onChoice`. The same resolver also pre-evaluates condition block groups — see [Condition blocks](/guide/block-types#condition) for details. - **`choice:` conditions** (referencing previous player selections) are resolved automatically by the engine via its internal choice history — the callback never sees them. - **Game-state conditions** (everything else) are delegated to the callback. - Chaining with `&` (AND) and `|` (OR) works correctly across both types. ## Filtering in onChoice In the handler, filter with one line: ### Why `visible !== false` and not `=== true`? When **no resolver is installed**, `visible` is `undefined`. Since `undefined !== false` evaluates to `true`, all choices pass — backward compatible by default. When a resolver **is installed**, choices are tagged `true` or `false` explicitly. | `visible` value | Meaning | `!== false` | |---|---|---| | `true` | Resolver installed, choice passes | `true` | | `false` | Resolver installed, choice hidden | `false` | | `undefined` | No resolver installed | `true` | ## RuntimeChoiceItem When a resolver is installed, each choice in `context.choices` is a `RuntimeChoiceItem` — an extension of `ChoiceItem` with the `visible` tag: ```ts [TypeScript] interface RuntimeChoiceItem extends ChoiceItem { visible?: boolean; // true | false | undefined } ``` ```csharp [C#] public class RuntimeChoiceItem : ChoiceItem { public bool? Visible { get; set; } // true | false | null } ``` ```cpp [C++] struct RuntimeChoiceItem : ChoiceItem { std::optional visible; // true | false | nullopt }; ``` ```gdscript [GDScript] # RuntimeChoiceItem is a Dictionary with an extra "visible" key: # { "uuid": "...", "dialogueText": {...}, "visible": true/false/absent } ``` Without a resolver, choices are still `RuntimeChoiceItem` but `visible` remains `undefined`/`null`/`nullopt`/absent. ## Examples ### Standard — show visible choices ```ts [TypeScript] engine.onChoice(({ context, next }) => { const visible = context.choices.filter(c => c.visible !== false); ui.showChoices(visible, (uuid) => { context.selectChoice(uuid); next(); }); }); ``` ```csharp [C#] engine.OnChoice(args => { var visible = args.Context.Choices .Where(c => c.Visible != false).ToList(); ShowChoicesUI(visible, uuid => { args.Context.SelectChoice(uuid); args.Next(); }); return null; }); ``` ```cpp [C++] engine.onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn { std::vector visible; for (const auto& c : ctx->choices()) if (!c.visible.has_value() || c.visible.value()) visible.push_back(&c); showChoicesUI(visible, [ctx, next](const auto& uuid) { ctx->selectChoice(uuid); next(); }); return {}; }); ``` ```gdscript [GDScript] engine.on_choice(func(args): var visible = [] for c in args["context"].choices: if c.get("visible") != false: visible.append(c) show_choices_ui(visible, func(uuid): args["context"].select_choice(uuid) args["next"].call() ) return Callable() ) ``` ### Timed choice — auto-select on timeout ```ts [TypeScript] engine.onChoice(({ block, context, next }) => { const visible = context.choices.filter(c => c.visible !== false); const timeout = block.nativeProperties?.timeout; const resolve = (choice) => { context.selectChoice(choice.uuid); next(); }; if (timeout) { const timer = setTimeout(() => resolve(visible[0]), timeout * 1000); ui.showChoices(visible, (uuid) => { clearTimeout(timer); resolve(visible.find(c => c.uuid === uuid)); }); } else { ui.showChoices(visible, (uuid) => resolve(visible.find(c => c.uuid === uuid))); } }); ``` ```csharp [C#] engine.OnChoice(args => { var (_, block, context, next) = args; var visible = context.Choices .Where(c => c.Visible != false).ToList(); var timeout = block.NativeProperties?.Timeout; void Resolve(RuntimeChoiceItem choice) { context.SelectChoice(choice.Uuid); next(); } if (timeout.HasValue) { // use your engine's timer — cancel on player selection var timer = ScheduleTimer((float)timeout.Value, () => Resolve(visible[0])); ShowChoicesUI(visible, uuid => { timer.Cancel(); Resolve(visible.First(c => c.Uuid == uuid)); }); } else { ShowChoicesUI(visible, uuid => Resolve(visible.First(c => c.Uuid == uuid))); } return null; }); ``` ```cpp [C++] engine.onChoice([](auto*, auto* block, auto* ctx, auto next) -> CleanupFn { std::vector visible; for (const auto& c : ctx->choices()) if (!c.visible.has_value() || c.visible.value()) visible.push_back(&c); auto timeout = block->nativeProperties ? block->nativeProperties->timeout : std::nullopt; auto resolve = [ctx, next](const std::string& uuid) { ctx->selectChoice(uuid); next(); }; if (timeout.has_value()) { // use your engine's timer — cancel on player selection auto timer = scheduleDelay(timeout.value(), [&]() { resolve(visible[0]->uuid); }); showChoicesUI(visible, [resolve, timer](const auto& uuid) { timer->cancel(); resolve(uuid); }); } else { showChoicesUI(visible, resolve); } return {}; }); ``` ```gdscript [GDScript] engine.on_choice(func(args): var ctx = args["context"] var next_fn = args["next"] var block = args["block"] var visible = [] for c in ctx.choices: if c.get("visible") != false: visible.append(c) var timeout_val = block.get("nativeProperties", {}).get("timeout", 0) if timeout_val > 0: # use your engine's timer — cancel on player selection var timer = get_tree().create_timer(timeout_val) timer.timeout.connect(func(): ctx.select_choice(visible[0]["uuid"]) next_fn.call() ) show_choices_ui(visible, func(uuid): timer.time_left = 0 # cancel ctx.select_choice(uuid) next_fn.call() ) else: show_choices_ui(visible, func(uuid): ctx.select_choice(uuid) next_fn.call() ) return Callable() ) ``` ### Hidden choices displayed greyed out ```ts [TypeScript] engine.onChoice(({ context, next }) => { for (const choice of context.choices) { if (choice.visible === false) { ui.addGreyed(choice); // show but disabled } else { ui.addNormal(choice); // selectable } } // wait for player selection... }); ``` ```csharp [C#] engine.OnChoice(args => { foreach (var choice in args.Context.Choices) { if (choice.Visible == false) AddGreyed(choice); // show but disabled else AddNormal(choice); // selectable } // wait for player selection... return null; }); ``` ```cpp [C++] engine.onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn { for (const auto& choice : ctx->choices()) { if (choice.visible.has_value() && !choice.visible.value()) addGreyed(choice); // show but disabled else addNormal(choice); // selectable } // wait for player selection... return {}; }); ``` ```gdscript [GDScript] engine.on_choice(func(args): for choice in args["context"].choices: if choice.get("visible") == false: add_greyed(choice) # show but disabled else: add_normal(choice) # selectable # wait for player selection... return Callable() ) ``` ### Tutorial — ignore visibility entirely ```ts [TypeScript] tutorial.onChoice(({ context, next }) => { // force-select the first choice, no filtering context.selectChoice(context.choices[0].uuid); next(); }); ``` ```csharp [C#] tutorial.OnChoice(args => { // force-select the first choice, no filtering args.Context.SelectChoice(args.Context.Choices[0].Uuid); args.Next(); return null; }); ``` ```cpp [C++] tutorial->onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn { // force-select the first choice, no filtering ctx->selectChoice(ctx->choices()[0].uuid); next(); return {}; }); ``` ```gdscript [GDScript] tutorial.on_choice(func(args): # force-select the first choice, no filtering args["context"].select_choice(args["context"].choices[0]["uuid"]) args["next"].call() return Callable() ) ``` ## Sharing the Evaluator With `onResolveCondition`, a single callback handles **both** choice visibility and condition block pre-evaluation. No more duplicating logic: > TIP: Before `onResolveCondition`, the same `gameState.check(...)` logic had to be registered in both `setChoiceFilter` and `onCondition` separately. With the unified resolver, it's one callback — the engine handles both automatically. ## Advanced: Manual Filtering If a global resolver is not desired, `LsdeUtils` provides a low-level utility: ```ts [TypeScript] import { LsdeUtils } from '@lsde/dialog-engine'; const visible = LsdeUtils.filterVisibleChoices( block.choices ?? [], (cond) => gameState.check(cond.key, cond.operator, cond.value), scene, // optional — enables choice: condition resolution via history ); ``` ```csharp [C#] var visible = LsdeUtils.FilterVisibleChoices( block.Choices ?? new(), cond => GameState.Check(cond.Key, cond.Operator, cond.Value), scene // optional — enables choice: condition resolution via history ); ``` ```cpp [C++] auto visible = lsde::LsdeUtils::FilterVisibleChoices( block->choices, [](const auto& cond) { return gameState.check(cond.key, cond.op, cond.value); }, scene // optional — enables choice: condition resolution via history ); ``` ```gdscript [GDScript] var visible = LsdeUtils.filter_visible_choices( block.get("choices", []), func(cond): return game_state.check(cond), scene # optional — enables choice: condition resolution via history ) ``` The `scene` parameter enables automatic `choice:` condition resolution. Without it, all conditions are delegated to the evaluator callback. ================================================================================ # Handlers ## Handlers Handlers are the bridge between the engine and your game. They work like observers — you subscribe a function, and the engine calls it when the matching event occurs. This is how you trigger the right behaviors in your game engine: display text, play an animation, evaluate state, etc. The engine exposes the following handlers: | Handler | Level | Description | |---------|-------|-------------| | [`onDialog`](/api-ref/classes/DialogueEngine#ondialog) | global / scene | Dialog block — display text | | [`onChoice`](/api-ref/classes/DialogueEngine#onchoice) | global / scene | Choice block — present choices | | [`onCondition`](/api-ref/classes/DialogueEngine#oncondition) | global / scene | Condition block — evaluate and branch | | [`onAction`](/api-ref/classes/DialogueEngine#onaction) | global / scene | Action block — trigger side effects | | [`onResolveCharacter`](/api-ref/classes/DialogueEngine#onresolvecharacter) | global / scene | Resolve which character is speaking | | [`onBeforeBlock`](/api-ref/classes/DialogueEngine#onbeforeblock) | global | Before every block (delay, entry animations…) | | [`onValidateNextBlock`](/api-ref/classes/DialogueEngine#onvalidatenextblock) | global | Validate before progressing to a block | | [`onInvalidateBlock`](/api-ref/classes/DialogueEngine#oninvalidateblock) | global | React when validation fails | | [`onSceneEnter`](/api-ref/classes/DialogueEngine#onsceneenter) | global / scene | A scene starts | | [`onSceneExit`](/api-ref/classes/DialogueEngine#onsceneexit) | global / scene | A scene ends | | [`onBlock`](/api-ref/interfaces/SceneHandle#onblock) | scene | Override a specific block by UUID | | [`onDialogId`](/api-ref/interfaces/SceneHandle#ondialogid) | scene | Override a specific DIALOG block by UUID (type-safe) | | [`onChoiceId`](/api-ref/interfaces/SceneHandle#onchoiceid) | scene | Override a specific CHOICE block by UUID (type-safe) | | [`onConditionId`](/api-ref/interfaces/SceneHandle#onconditionid) | scene | Override a specific CONDITION block by UUID (type-safe) | | [`onActionId`](/api-ref/interfaces/SceneHandle#onactionid) | scene | Override a specific ACTION block by UUID (type-safe) | | [`onResolveCondition`](/api-ref/classes/DialogueEngine#onresolvecondition) | global | Unified condition resolver (choice visibility + condition pre-evaluation) | | ~~[`setChoiceFilter`](/api-ref/classes/DialogueEngine#setchoicefilter)~~ | global | _Deprecated — use `onResolveCondition` instead_ | `onDialog`, `onChoice`, and `onAction` are **required** — the engine validates their presence when `start()` is called and throws a descriptive error if any are missing. `onCondition` is **optional** when `onResolveCondition` is installed — the engine auto-routes from pre-evaluated condition groups. ## Two-Tier Handler System The engine resolves handlers in two tiers: - **Global handlers** — registered on the engine, they define the default behavior for every scene. They are typically all you need. - **Scene handlers** — registered on a specific [`SceneHandle`](/api-ref/interfaces/SceneHandle), they let you override or extend the default behavior when a scene requires a different rendering or control flow. This is rare, but available. When a block is dispatched, the engine resolves the handler in this order: 1. `handle.onBlock(uuid)` or `handle.onDialogId(uuid)` / `handle.onActionId(uuid)` / ... — block-specific override 2. `handle.onDialog()` / `handle.onChoice()` / ... — scene-level type handler 3. `engine.onDialog()` / `engine.onChoice()` / ... — global handler When both tiers are present, both run in sequence — scene first, then global — unless the scene handler calls `context.preventGlobalHandler()` to suppress the global pass. ## Character Resolution Character resolution is optional. By registering an `onResolveCharacter` callback, the engine invokes it before every block that has characters in its `metadata.characters`. The callback receives the list of characters assigned to the block and returns the one that should be active — or `undefined` if none is available. The resolved character is then accessible via `context.character` in all handlers. This is the ideal integration point to query your game state: check if a character is present in the scene, alive, in camera range, etc. Returning `undefined` opens the door to several strategies: skip the block via [`skipIfMissingActor`](/api-ref/interfaces/NativeProperties#skipifmissingactor), cancel the scene via `handle.cancel()`, or handle the case directly in the handler. ## Scene Lifecycle The `onSceneEnter` and `onSceneExit` callbacks let you react to a scene starting and ending — enable cinema mode, freeze NPCs, prepare the UI, clean up resources, etc. They are available at global level (on the engine) and at scene level (via `handle.onEnter()` / `handle.onExit()`). The scene handler replaces the global one if defined. ## Block Override `onBlock(uuid)` lets you target a specific block by its identifier and assign it a dedicated handler. This is a rare use case — generic handlers cover the vast majority of needs — but for very specific scenarios where an individual block requires distinct behavior, it is available. ## Type-Safe Block Override `onDialogId(uuid)`, `onChoiceId(uuid)`, `onConditionId(uuid)`, and `onActionId(uuid)` are type-safe alternatives to `onBlock(uuid)`. They work exactly the same way — same priority, same `preventGlobalHandler` support — but the handler receives the specialized block type and context instead of the generic union. Use these when you know the block type at registration time and want full autocompletion on `block` and `context`. ## Visual Reference ### Two-Tier Handler Dispatch ```mermaid flowchart TD A[block dispatched] --> B{resolve scene handler} B --> B1{"onBlock(uuid) /\nonDialogId(uuid) etc.?"} B1 -- found --> S B1 -- not found --> B2{"handle.onDialog() etc.?"} B2 -- found --> S B2 -- not found --> G S[execute scene handler] --> D{preventGlobalHandler?} D -- yes --> Z[done] D -- no --> G["execute global handler\nengine.onDialog() etc."] G --> Z ``` ================================================================================ # Game Engine Integration LSDE is engine-agnostic — no dependency on any game engine, UI framework, or audio system. It walks a graph and calls your handlers. This page shows how to wire it into the most common game engines. For detailed handler implementation, see [Block Types](./block-types) and [Handlers](./handlers). ## Full Integration The following example shows one way to integrate LSDE into each engine. It covers the 4 required handlers — dialog, choice, condition, action — in a single class, as a starting point. Every game has its own needs. Adapt the structure, the layout, and the UI to your project. ## The 4 Handlers Each handler receives the block data and a `next()` callback. The developer processes the data in their engine, then calls `next()` when the block is done. The timing of that call belongs entirely to the game. - **Dialog** — text, character, native properties. Display the dialogue in your UI, wait for player input or a delay, then call `next()`. Return a cleanup function to hide the UI when the engine moves to the next block. - **Choice** — list of choices tagged `visible` when a `choiceFilter` is configured. Create the corresponding UI elements — buttons, list, radial menu. On player selection, `selectChoice(uuid)` tells the engine which branch to follow, then `next()` advances the flow. - **Condition** — conditions defined in the block. Evaluate them with your game logic — check a flag, a quest, an inventory. `context.resolve(true)` sends the flow to port 0, `context.resolve(false)` to port 1. - **Action** — actions defined in the block. Execute them in your engine — play a sound, give an item, trigger a cinematic. `context.resolve()` confirms success, `context.reject(err)` signals failure. ## Tips - **`next()` is the remote control.** Call it instantly for rapid-fire dialogue, or hold it until an animation finishes. The engine waits — it has no concept of time. - **Cleanup functions clean up after you.** Return a function from any handler — the engine calls it when moving to the next block. Perfect for hiding UI, stopping audio, or freeing nodes. - **`onBeforeBlock` handles delays.** The engine does not enforce `nativeProperties.delay` — `onBeforeBlock` reads it and calls `resolve()` after a timer. Full control. - **Async tracks are parallel flows.** When a cutscene needs dialogue and camera movement at the same time, blocks marked `isAsync` in the editor run on independent tracks. ================================================================================