Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Execution Model

A compiled story runs as a synchronous step function: it executes bytecode until it reaches a yield point, then hands back a Line. The variant of that Line tells you what just happened and what to do next. This loop is the shared foundation under every client — raw Rust, Bevy, the web runner all express the same model.

use brink_runtime::{Line, Story};

let mut story = Story::new(&program, line_tables);

loop {
    match story.continue_single()? {
        // Mid-stream content; more may follow this turn.
        Line::Text { text, tags } => print!("{text}"),
        // This turn's output is complete (`-> DONE`); keep stepping.
        Line::Done { text, tags } => print!("{text}"),
        Line::Choices { text, tags, choices } => {
            print!("{text}");
            // Present `choices`, get the player's selection...
            story.choose(chosen_index)?;
        }
        Line::End { text, tags } => {
            print!("{text}");
            break;
        }
    }
}

Story is the mutable half of the two-object model: it borrows an immutable Program and carries all the execution state.

Line variants

VariantMeaningNext action
Text { text, tags }One line of content. More may follow this turn.Call continue_single() again.
Done { text, tags }The turn’s output is complete (ink done). The story is not over.Call continue_single() again for the next turn.
Choices { text, tags, choices }The story is waiting for a choice.Call story.choose(index), then continue.
End { text, tags }The story reached -> END. Permanently finished.Stop stepping.

Every variant carries the text produced since the last yield point and any ink tags (# tag) attached to it. The helpers line.text(), line.tags(), and line.is_terminal() work across variants (is_terminal() is true for anything but Text).

continue_single vs continue_maximally

  • continue_single() -> Line produces one line — ideal for typewriter UIs that reveal content a line at a time.
  • continue_maximally() -> Vec<Line> runs until a terminal line and returns every line produced along the way; the last element is always a terminal variant (Done, Choices, or End). Ideal for click-to-continue UIs that show a whole passage at once.
loop {
    let lines = story.continue_maximally()?;
    for line in &lines {
        print!("{}", line.text());
    }
    match lines.last() {
        Some(Line::Choices { choices, .. }) => story.choose(choices[0].index)?,
        Some(Line::End { .. }) | None => break,
        _ => {} // Done — loop again for the next turn.
    }
}

Both have _with(&handler) variants (continue_single_with, continue_maximally_with) that take a custom ExternalFnHandler for external functions.

Choices

When the story yields Line::Choices, execution is blocked until you select one with story.choose(index):

Line::Choices { text, choices, .. } => {
    for choice in &choices {
        println!("{}: {}", choice.index + 1, choice.text);
    }
    story.choose(choices[selected].index)?;
}

Each Choice carries:

FieldTypeDescription
textStringdisplay text for this choice
indexusizethe value to pass to story.choose()
tagsVec<String>tags attached to this choice

Ink defines several choice kinds, but they’re resolved by the compiler and VM — the runtime always hands you a flat Vec<Choice> of the ones currently selectable:

  • Once-only (*) — the default; disappears after it’s taken.
  • Sticky (+) — stays available on later visits.
  • Fallback — has no display text; auto-selected when nothing else is available, and never appears in the choices vec.
  • Conditional — guarded by a condition; only present when the guard is true.

Choice-related errors (InvalidChoiceIndex, NotWaitingForChoice) are listed in Reference › Errors.

StoryStatus

You can query story.status() at any time:

StatusMeaning
ActiveReady to step.
WaitingForChoiceMust call choose() before stepping.
DoneHit a done opcode. Can resume with continue_single().
EndedHit -> END. Cannot step further.

Text accumulation

A story may produce several Text/Done lines before reaching Choices or End. Each continue_single() carries only the text since the previous yield. If your application needs the full passage, accumulate text across lines until a Choices or End arrives — or use continue_maximally(), which batches a whole passage for you.