Inkling

Partial implementation of the Ink markup language for branching game stories and dialogue in Rust.

Ink is a creation of Inkle. For more information about the language, see their website.

This book is a user’s guide to the language. It is written with the intent to be a mostly friendly and accessible explanation of getting it up and running. The more advanced documentation of the API is available at docs.rs.

License

inkling is copyleft, licensed under the Parity License. The code is available on Github. Contact us about the possibility of acquiring a private license.

Introduction

When writing a story for a game one has to consider how to get the script into it. This is simple if the script is in plain text, but depending on our game we might need to account for many possibilities, including but not limited to:

  • Using variables for names or items which are declared elsewhere
  • Branching the story into different paths
  • Testing conditions for presenting certain parts
  • Marking up content for emphasis or effect

Ink is a scripting language which implements many of these features in its design. inkling is a library which can read this language and present the final text to the reader. This chapter will introduce what the language is and how inkling works with it.

What is Ink?

Ink is a scripting language created by game developer Inkle to use in their many narrative driven games, including 80 Days, Heaven’s Vault and Pendragon.

It is designed to make it easy to write a script in regular text files, without the need for complicated tools (although Inkle does provide such tools). Since their games are narrative heavy, with dialogue and paths which change as the player moves through them, it has excellent support for branching the story into different parts. All this while still being very easy to read.

More information about the language can be found on Inkle’s web site. This includes several tutorials for getting familiar with both basic and advanced features of the language.

What is Inkling?

inkling is a library which reads stories written in Ink and presents their content to the user. It is an interface, not a game engine: it validates the script and returns the text, but it is up to the caller to take that text and use it however they want in their game.

Why use inkling?

  • Simple interface
  • Rust native
  • Support for non-latin alphabets in identifiers
  • Few dependencies: optional serde dependency for easy saving and loading, and optional rand dependency for adding randomized features

Why not?

  • Fewer features than Inkle’s implementation of the language
  • Untested in serious work loads and large scripts: expect bugs
  • Written by a hobbyist, who cannot promise quick fixes or support

Short example

This section provides an example of a script and how to read it into your program with inkling.

Script

// This is an `ink` script, saved as 'story.ink'

A single candle flickered by my side.
Pen in hand I procured a blank letter.

*   "Dear Guillaume"
    Sparing the more unfavorable details from him, I requested his aid.
    -> guillaume_arrives

*   "To the Fiendish Impostor"
    -> write_to_laurent

=== guillaume_arrives ===
A few days later my servant informed me of Guillaume's arrival. 
I met with him in the lounge.

=== write_to_laurent ===
The letter was spiked with insults and veiled threats.

Rust code

extern crate inkling;
use std::fs::read_to_string;
use inkling::read_story_from_string;

// Read the script into memory
let story_content = read_to_string("story.ink").unwrap();

// Read the story from the script
let mut story = read_story_from_string(&story_content).unwrap();

The next chapter will explain how to proceed from here.

Using Inkling

This chapter goes into more detail how to use inkling in your program. It also comes with a couple of examples of how run it.

If you want to dive even deeper into the functions, structures and methods, please check out the full documentation.

Set-up

To use inkling in your Rust project, add this line to your dependencies in Cargo.toml:

[dependencies]
#
inkling = "1.0.0-pre.1"

By default, inkling has no additional dependencies. Extra features which carry dependencies can be opted in to.

Adding serde support

The serde library is widely used to serialize and deserialize data, which can be used to save and restore an object. Support for this can be added to inkling by enabling the serde_support feature. This adds serde as a dependency.

[dependencies]
#
inkling = { version = "1.0.0-pre.1", features = ["serde_support"] }

Randomization support

The Ink language supports a few randomized features like shuffle sequences. These are optional and can be enabled with the random feature. This adds a dependency to rand and its sub project rand_chacha.

[dependencies]
#
inkling = { version = "1.0.0-pre.1", features = ["random"] }

If this feature is not enabled, shuffle sequences will behave as cycle sequences.

Reading a script

Let us now introduce how to move through a script with inkling. We will reuse the script from the short introductory example:


#![allow(unused)]
fn main() {
let content = r#"
A single candle flickered by my side.
Pen in hand I procured a blank letter.

*   "Dear Guillaume"
    Sparing the more unfavorable details from him, I requested his aid.

*   "To the Fiendish Impostor"
"#;
}

Reading text content

To parse your script into a story, inkling provides the read_story_from_string function. It takes the script (as a string) and from that returns a Story object in a Result.

Again, we parse it into a story:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Story};
let content = r#"
A single candle flickered by my side.
Pen in hand I procured a blank letter.

*   "Dear Guillaume"
    Sparing the more unfavorable details from him, I requested his aid.

*   "To the Fiendish Impostor"
"#;
let mut story: Story = read_story_from_string(&content).unwrap();
}

Aside: The Story object

Story contains the entire parsed script in a form that is ready to be used. It implements a wealth of methods which can be used to go through it or modify its state by setting variables and changing locations. Look through the documentation for the object for more information about these methods.

Starting the story

To start the story we must supply a buffer which it can add text lines into. The story will then proceed until a set of choices is encountered, which the user has to select from.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Story};
let content = r#"
A single candle flickered by my side.
Pen in hand I procured a blank letter.

*   "Dear Guillaume"
    Sparing the more unfavorable details from him, I requested his aid.

*   "To the Fiendish Impostor"
"#;
let mut story: Story = read_story_from_string(&content).unwrap();
use inkling::Line;

// Buffer which the text lines will be added to
let mut line_buffer: Vec<Line> = Vec::new();

// Begin the story by calling `resume`
let result = story.resume(&mut line_buffer).unwrap();

// The two first lines have now been added to the buffer
assert_eq!(line_buffer[0].text, "A single candle flickered by my side.\n");
assert_eq!(line_buffer[1].text, "Pen in hand I procured a blank letter.\n");
assert_eq!(line_buffer.len(), 2);
}

Note that the lines end with newline characters to denote that they are separate paragraphs. Oh, and the text lines are of type Line, which contains two fields: text (seen above) and tags for tags which are associated with the line.

Encountering choices

The story returned once it encountered the choice of whom to pen a letter to. This set of choices is present in the returned object, which is an enum of type Prompt. We can access the choices (which are of type Choice) through this object.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Story, Prompt};
let content = r#"
A single candle flickered by my side.
Pen in hand I procured a blank letter.

*   "Dear Guillaume"
    Sparing the more unfavorable details from him, I requested his aid.

*   "To the Fiendish Impostor"
"#;
let mut story: Story = read_story_from_string(&content).unwrap();
let mut line_buffer = Vec::new();
let result = story.resume(&mut line_buffer).unwrap();
match result {
    Prompt::Choice(choices) => {
        assert_eq!(choices[0].text, r#""Dear Guillaume""#);
        assert_eq!(choices[1].text, r#""To the Fiendish Impostor""#);
        assert_eq!(choices.len(), 2);
    }
    Done => (),
}
}

To continue the story we use the make_choice method with an index corresponding to Choice made.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Story, Prompt};
let content = r#"
A single candle flickered by my side.
Pen in hand I procured a blank letter.

*   "Dear Guillaume"
    Sparing the more unfavorable details from him, I requested his aid.

*   "To the Fiendish Impostor"
"#;
let mut story: Story = read_story_from_string(&content).unwrap();
let mut line_buffer = Vec::new();
let result = story.resume(&mut line_buffer).unwrap();
story.make_choice(0).unwrap();

let result = story.resume(&mut line_buffer).unwrap();

assert!(line_buffer[2].text.starts_with(r#""Dear Guillaume""#));
assert!(line_buffer[3].text.starts_with("Sparing the more unfavorable details"));
}

Note that inkling does not clear the supplied buffer when resuming the story. That task is trusted to you, if you need to, by running line_buffer.clear().

Summary

Design intent

This library has been written with the intent to create a simple usage loop.

while let Prompt::Choice(choices) = story.resume(&mut buffer)? {
    // Process text, show it to the player, then present the encountered
    // choices to them and resume.
    let i = select_choice(&choices)?;
    story.make_choice(i)?;
}

The loop will finish when Prompt::Done is returned from the resume call, signaling the end of the story. Here errors are returned through the standard ? operator, which further simplifies the loop.

Of course, this pattern may not suit your application.

Helper functions

This page lists miscellaneous functions to deal with inkling data.

Text handling

  • copy_lines_into_string takes a buffer of Line objects and joins the text into a single string which is returned

Read error handling

  • print_read_error creates a string with the information of all errors that were encountered when parsing a story

Inspecting the log

When the Story is parsed it goes through all lines and scenes in the script, inspecting them for inconsistencies and notes. If any errors are encountered the parsing fails, and an error is returned.

However, some inconsistencies are not very serious and do not raise an error. Such issues are instead added to a log, which you can inspect after the parsing has finished. You can then decide whether any yielded warning is sufficient for further investigation.

We recommend that you always check this log and inspect its messages after parsing a story. Besides regular warnings, it will also contain reminders of any to-do comment you have added to the story, which may merit having a look at.

The log supports standard iterator operations, which makes it simple to walk through all warnings and to-do comments at once.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Story};
let content = r#"
TODO: Should these names be in variables?
A single candle flickered by my side.
Pen in hand I procured a blank letter.

*   "Dear Guillaume"
    Sparing the more unfavorable details from him, I requested his aid.

*   "To the Fiendish Impostor"
"#;
let mut story: Story = read_story_from_string(&content).unwrap();

// Print all warnings and comments to standard error for inspection
for message in story.log.iter() {
    eprintln!("{}", message);
}

assert_eq!(story.log.todo_comments.len(), 1);
}

Dealing with errors

Errors from reading the Story

When parsing the Story using read_story_from_string, its content is validated. This means that inkling goes through it and looks for errors. These errors include things like invalid knot or variable declarations, using invalid names for variables in assignments and conditions, wrongly typed conditions, and much more.

If any error is encountered during this validation step, the function returns a ReadError which contains a list of all the errors it found. The helper function print_read_error exists to write a description of all errors and where they were found into a single string, which can be written to a log file.

Runtime errors

Once a story is started, returned errors will be of InklingError type.

Saving and loading

Saving and loading the state of a story can be done using the serialization and deserialization methods of serde.

If the serde_support feature is enabled (see Set-up for more information), inkling derives the required method for all of its objects. It is then possible to use any compatible serializer and deserializer to save the state into some object on disk, and restore the data from that object.

Some supported data formats are listed on this page.

Example: using JSON

The serde_json crate uses JSON text files as storage. In Cargo.toml, add

serde_json = "1.0"

to your dependencies and ensure that the serde_support feature is enabled for inkling.

Converting from Story to String

use serde_json;

let serialized_story: String = serde_json::to_string(&story).unwrap();

// write `serialized_story` to a text file

Restoring a Story from String

use serde_json;

// `serialized_story` is read from a text file

let story: Story = serde_json::from_str(&serialized_story).unwrap();

Example: Text adventure

This page contains a sample implementation of a simple story reader using inkling. It only uses plain text and a terminal.

The full code can be found as the player.rs example on Github.

Design

The player requires this functionality:

  • Reading a story from a file
  • A game loop
  • Presenting the text to the player
  • Asking the player for a choice at branches

Reading

A simple function that attempts to read the story from a file at a given path. If errors are encountered they should be handled.


#![allow(unused)]
fn main() {
extern crate inkling;
use std::io::Write;
use inkling::{error::parse::print_read_error, read_story_from_string, Story};

fn read_story(path: &std::path::Path) -> Result<Story, std::io::Error> {
    let content = std::fs::read_to_string(path)?;

    match read_story_from_string(&content) {
        Ok(story) => Ok(story),
        Err(error) => {
            // If the story could not be parsed, write the list of errors to stderr
            write!(
                std::io::stderr(),
                "{}",
                print_read_error(&error).unwrap()
            )
            .unwrap();

            std::process::exit(1);
        }
    }
}
}

Game loop

The main loop implements the standard pattern.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{InklingError, Prompt, Story};

fn play_story(mut story: Story) -> Result<(), InklingError> {
    let mut line_buffer = Vec::new();

    while let Prompt::Choice(choices) = story.resume(&mut line_buffer)? {
        print_lines(&line_buffer);
        line_buffer.clear();

        let choice = ask_user_for_choice(&choices).unwrap_or_else(|| {
            println!("Exiting program.");
            std::process::exit(0);
        });

        println!("");
        story.make_choice(choice)?;
    }

    Ok(())
}

// Mock the following functions
fn print_lines(buffer: &inkling::LineBuffer) { unimplemented!(); }
fn ask_user_for_choice(choices: &[inkling::Choice]) -> Option<usize> { unimplemented!(); }
}

Printing story text

Simply iterate through the list of lines and print the text. Add an extra newline if there is a paragraph break.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::LineBuffer;

fn print_lines(lines: &LineBuffer) {
    for line in lines {
        print!("{}", line.text);

        if line.text.ends_with('\n') {
            print!("\n");
        }
    }
}
}

Asking the player for a choice

Print the available choices one by one, then ask for a selection.


#![allow(unused)]
fn main() {
extern crate inkling;
use std::io;
use inkling::Choice;

fn ask_user_for_choice(choices: &[Choice]) -> Option<usize> {
    println!("Choose:");

    for (i, choice) in choices.iter().enumerate() {
        println!("  {}. {}", i + 1, choice.text);
    }

    println!("     ---");
    println!("  0. Exit story");
    println!("");

    let index = get_choice(choices.len())?;
    Some(index)
}

fn get_choice(num_choices: usize) -> Option<usize> {
    loop {
        let mut input = String::new();
        std::io::stdin().read_line(&mut input).unwrap();

        match input.trim().parse::<usize>() {
            Ok(0) => {
                return None;
            }
            Ok(i) if i > 0 && i <= num_choices => {
                return Some(i - 1);
            }
            _ => {
                println!("Not a valid option, try again:");
            }
        }
    }
}
}

Implemented Ink features

This chapter contains a list of the Ink features which are available in inkling.

More information about these features can be found in Inkle’s guide to writing with Ink, which is a better guide showing how to write a script.

However, not everything in that guide can be done with inkling, since it is not completely compatible with the original implementation. This is partly the reason for this document, which shows which features are guaranteed to work. All examples shown here are accompanied under the hood by tests which assert that the result is what it should be.

Examples in this chapter show how the features are written in plain .ink text files (although the file names do not have to end with .ink). Text inside of these files will appear like this:


#![allow(unused)]
fn main() {
let content = "
Example text in a file to be read.
";
}

Major features which are not yet implemented are listed on the missing features page.

Basic elements

These are the basic features needed to write a story with Ink and inkling.

Text

Plain text is the most basic element of a story. It is written in the story text as regular lines.


#![allow(unused)]
fn main() {
let content = r"

I opened my notebook to a blank page, pen in hand.

";
}

Text is separated into paragraphs by being on different lines.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"

My hand moved towards the canvas.
The cold draft made a shudder run through my body.
A dark blot spread from where my pen was resting.

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(buffer[0].text, "My hand moved towards the canvas.\n");
assert_eq!(buffer[1].text, "The cold draft made a shudder run through my body.\n");
assert_eq!(buffer[2].text, "A dark blot spread from where my pen was resting.\n");
}

Those three lines will be returned from inkling as separate lines, each ending with a newline character.

Glue

If you want to remove the newline character from in between lines, you can use the <> marker which signifies glue. This:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"

This line will <>
be glued to this, without creating a new paragraph.

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert!(!buffer[0].text.ends_with("\n"));
}

Becomes:

This line will be glued to this, without creating a new paragraph.

as will this, since glue can be put at either end:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"

This line will 
<> be glued to this, without creating a new paragraph.

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert!(!buffer[0].text.ends_with("\n"));
}

For these examples glue doesn’t do much, but it will be more useful once we introduce story structure features. Keep it in mind until then.

Comments

The text file can contain comments, which will be ignored by inkling as it parses the story. To write a comment, preceed the line with //.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"

The cold could not be ignored.
// Unlike this line, which will be 
As will the end of this. // removed comment at end of line

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(buffer[0].text, "The cold could not be ignored.\n");
assert_eq!(buffer[1].text, "As will the end of this.\n");
}

Note that multiline comments with /* and */ are not currently supported.

Branching story paths

To mark a choice in a branching story, use the * marker.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"

*   Choice 1
*   Choice 2 

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices[0].text, "Choice 1");
      assert_eq!(choices[1].text, "Choice 2");
  } 
  _ => unreachable!()
}
}

(The + marker can also be used, which results in a different behavior if the tree is visited again. More on this later.)

When inkling encounters one or more lines beginning with this marker, the options will be collected and returned to the user to make a choice.

After making a choice, the story proceeds from lines below the choice. So this story:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r#"

A noise rang from the door.
*   "Hello?" I shouted.
    "Who's there?"
*   I rose from the desk and walked over.

"#;
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices[0].text, r#""Hello?" I shouted."#);
      assert_eq!(choices[1].text, r"I rose from the desk and walked over.");
  } 
  _ => unreachable!()
}
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert!(buffer[0].text.starts_with(r#"A noise rang from the door."#));
assert!(buffer[1].text.starts_with(r#""Hello?" I shouted."#));
assert!(buffer[2].text.starts_with(r#""Who's there?""#));
}

results in this “game” for the user (in this case picking the first option):

A noise rang from the door.
 1: "Hello?" I shouted.
 2: I rose from the desk and walked over.

> 1
"Hello?" I shouted.
"Who's there?"

Removing choice text from output

As the previous example show, by default, the choice text will be added to the text presented to the user. Text encased in square brackets [] will, however, be ignored. Building on the previous example:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r#"

*   ["Hello?" I shouted.]
    "Who's there?"

"#;
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices[0].text, r#""Hello?" I shouted."#);
  } 
  _ => unreachable!()
}
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert!(buffer[0].text.starts_with(r#""Who's there?""#));
}
 1: "Hello?" I shouted.

> 1
"Who's there?"

Note how the choice text is not printed below the selection.

Advanced: mixing choice and presented text

The square brackets also acts as a divider between choice and presented text. Any text after the square brackets will not appear in the choice text. Text before the brackets will appear in both choice and output text. This makes it easy to build a simple choice text into a more presentable sentence for the story:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r#"

*   "Hello[?"]," I shouted. "Who's there?"

"#;
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices[0].text, r#""Hello?""#);
  } 
  _ => unreachable!()
}
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert!(buffer[0].text.starts_with(r#""Hello," I shouted. "Who's there?""#));
}
 1: "Hello?"

> 1
"Hello," I shouted. "Who's there?"

Nested dialogue options

Dialogue branches can be nested, more or less infinitely. Just add extra * markers to specify the depth.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"

*   Choice 1
    * *     Choice 1.1
    * *     Choice 1.2
            * * *   Choice 1.2.1
*   Choice 2
    * *     Choice 2.1
    * *     Choice 2.2

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
story.make_choice(0).unwrap();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(&choices[0].text, "Choice 1.1");
      assert_eq!(&choices[1].text, "Choice 1.2");
      story.make_choice(1).unwrap();
      match story.resume(&mut buffer).unwrap() {
          Prompt::Choice(choices) => {
              assert_eq!(&choices[0].text, "Choice 1.2.1");
          }
          _ => unreachable!()
      }
  }
  _ => unreachable!()
}
}

Any extra whitespace is just for readability. The previous example produces the exact same tree as this, much less readable, example:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content_nowhitespace = r"

*Choice 1
**Choice 1.1
**Choice 1.2
***Choice 1.2.1
*Choice 2
**Choice 2.1
**Choice 2.2

";
let content_whitespace = r"

*   Choice 1
    * *     Choice 1.1
    * *     Choice 1.2
            * * *   Choice 1.2.1
*   Choice 2
    * *     Choice 2.1
    * *     Choice 2.2

";

let story_nowhitespace = read_story_from_string(content_nowhitespace).unwrap();
let story_whitespace = read_story_from_string(content_whitespace).unwrap();
assert_eq!(format!("{:?}", story_nowhitespace), format!("{:?}", story_whitespace));
}

Story structure

Story scripts can be divided into different sections, to which the story can diverge. This section introduces how to create these sections and move to them in the text.

Knots

A story can be divided into different sections, called knots in Ink. This division is invisible to the user but makes it easier to write and reason about the story in production.

A knot is denoted by beginning the line with at least two (2) = signs followed by a name for the knot. On the following lines, the story text can resume.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Location};
let content = r"

== stairwell
I made my way down the empty stairwell.

";
let mut story = read_story_from_string(content).unwrap();
assert!(story.move_to(&Location::from("stairwell")).is_ok());
}

The name (’stairwell’ in the previous example) cannot contain spaces or non-alphanumeric symbols. Optionally, it may be followed by more = signs, which are not necessary but may make it easier to identify knots in the document. This is identical to the previous example:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Location};
let content = r"

=== stairwell ===
I made my way down the empty stairwell.

";
let mut story = read_story_from_string(content).unwrap();
assert!(story.move_to(&Location::from("stairwell")).is_ok());
}

Non-latin characters

Knot names support any character as long as they are alphanumeric according to the Rust language specification. This seems to include all languages which are recognized by UTF-8. Thus, knots (and any identifer) may contain e.g. Chinese, Japanese, Arabic, Cyrillic and other characters. Do let us know if you find any exceptions.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Location};
let content = r"

=== عقدة ===
These

=== 매듭 ===
are

=== गांठ ===
all

=== 結 ===
allowed.

";
let mut story = read_story_from_string(content).unwrap();
assert!(story.move_to(&Location::from("عقدة")).is_ok());
assert!(story.move_to(&Location::from("매듭")).is_ok());
assert!(story.move_to(&Location::from("गांठ")).is_ok());
assert!(story.move_to(&Location::from("結")).is_ok());
}

Stitches

Knots may be further subdivided into stitches. These are denoted by single = markers.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Location};
let content = r"

=== garden ===
= entrance
A pale moonlight illuminated the garden.

= well
The well stank of stagnant water. Is that an eel I see at the bottom?

";
let mut story = read_story_from_string(content).unwrap();
assert!(story.move_to(&Location::from("garden.entrance")).is_ok());
assert!(story.move_to(&Location::from("garden.well")).is_ok());
}

Diverts

Diverts are used to move to different parts of the story. A divert to a knot moves the story to continue from there. They are designated with the -> marker followed by the destination.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Location};
let content = r"
-> stairwell

=== stairwell ===
The stairs creaked as I descended.
-> lower_floor

=== garden ===
A pale moonlight illuminated the garden as I entered it.
-> END

=== lower_floor ===
On the bottom I found an unlocked door.
-> garden

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(story.get_current_location(), Location::from("garden"));
}

Diverts are automatically followed as they are encountered.

Diverts to stitches

Stitches inside knots can be diverted to using knot.stitch as a destination:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"

-> garden.entrance

=== garden ===
= well
Unreachable.
= entrance
A pale moonlight illuminated the garden.
";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert!(buffer[0].text.starts_with("A pale moonlight illuminated the garden."));
}

Stitches within the same knot can be diverted to with only the stitch name:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"
-> garden

=== garden ===
-> well

= entrance
A pale moonlight illuminated the garden.

= well
The well stank of stagnant water.

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert!(buffer[0].text.starts_with("The well stank of stagnant water."));
}

Ending the story with -> END

END is a destination that signifies that the story has come to, well, an end. Use -> END diverts for such occasions. An ink story is not complete unless all branches one way or another leads to an -> END divert: ending a story should be intentional.

Diverts in choices

A common use of branches is to divert to other knots.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Location};
let content = r"

*   [Descend stairs] -> lower_floor
*   [Return to desk]
    I sighed, wearily, and returned to my room.
    -> desk

=== desk ===
As I sat by my desk, I noticed that my notebook had gone missing.

=== lower_floor ===
On the bottom I found an unlocked door.

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
story.make_choice(1).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(story.get_current_location(), Location::from("desk"));
}

Revisiting content and choices

With diverts we can easily return to previously visited knots and stitches. When this happens, the text is reevaluated to reflect the current state of the story (see the sections on conditional content and alternating sequences for more information).


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"

=== table ===
You are seated at the table.

*   [Order a cup of tea] 
    A waiter returns with a steaming hot cup of tea. 
    -> table
*   [Leave]
    You leave the café.

";
assert!(read_story_from_string(content).is_ok());
}

Once-only and sticky choices

Any set of branching choices will also be reevaluated. There are two types of choices, denoted by if they begin with * or + markers:

  • * marks once-only choices, which can only be picked once
  • + marks sticky choices, which can be picked any number of times

In short, once-only choices are removed from the choice list if they are picked. Sticky choices will remain. This has to be kept in mind if the branch might be revisited during the story.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"
-> loop

=== loop ===
*   This choice can only be picked once -> loop
+   This choice is always here -> loop

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 2);
      assert_eq!(&choices[0].text, "This choice can only be picked once");
  }
  _ => unreachable!()
}
story.make_choice(0).unwrap();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 1);
      assert_eq!(&choices[0].text, "This choice is always here");
  }
  _ => unreachable!()
}
story.make_choice(0).unwrap();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 1);
      assert_eq!(&choices[0].text, "This choice is always here");
  }
  _ => unreachable!()
}
}

Running out of choices

Since once-only choices are removed it is possible for a branching choice point to run out of choices. This will result in an error being returned from inkling at runtime.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"
-> thrice_fail

=== thrice_fail ===
The third time we visit this we are out of choices and an error is returned.

*   First choice -> thrice_fail
*   Second choice -> thrice_fail

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
story.make_choice(0).unwrap();
assert!(story.resume(&mut buffer).is_err());
}

So be careful when writing branching choices using only once-only markers. Is there a risk that you will return to it multiple times?

Fallback choices

There is a fallback option available for running out of choices. If no regular (sticky or once-only) choices are left to present for the user, inkling will look for a fallback choice and automatically follow it.

This can only be a single choice and is marked by being a choice without choice text, which is to say that it starts with a divert -> marker.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"
-> twice_fail

=== twice_fail ===
The second time we visit this we are out of regular choices.
We then use the fallback.

*   First choice -> twice_fail
*   -> fallback

=== fallback ===
We escaped the loop!

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 1);
      assert_eq!(&choices[0].text, "First choice");
  }
  _ => unreachable!()
}
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap(); 
assert_eq!(&buffer.last().unwrap().text, "We escaped the loop!\n");
}

The fallback content can contain text by putting it on a new line directly after the divert marker.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"
-> write_article

=== write_article ===
*   [Write abstract] -> write_article
*   [Write main text] -> write_article
*   [Write summary] -> write_article
*   -> 
    The article is finished.
    -> submit_article

=== submit_article ===
You submit it to your editor. Wow, writing is easy!

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap(); 
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap(); 
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap(); 
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap(); 
assert_eq!(&buffer[0].text, "The article is finished.\n");
assert!(&buffer[1].text.starts_with("You submit it to your editor."));
}

Fallback choices can also be sticky. If they are not they will also be consumed after use. Again, ensure that you are sure that branches with non-sticky fallback choices will not be returned to multiple times.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Location, Prompt};
let content = r"

=== once_only_fallback ===
This will return an error if the fallback choice is used twice.
*   -> once_only_fallback 

=== sticky_fallback ===
{sticky_fallback > 4 : -> END} // exit once we have returned here a few times
This sticky fallback choice can be use any number of times.
+   -> sticky_fallback 

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.move_to(&Location::from("once_only_fallback")).unwrap();
assert!(story.resume(&mut buffer).is_err());
story.move_to(&Location::from("sticky_fallback")).unwrap();
story.resume(&mut buffer).unwrap(); 
assert!(story.resume(&mut buffer).is_ok());
}

Gather points

When creating a set of choices, you can return (or, gather) all of the branches to a single path after they have gone through their content. This is done using gather points.

To return the branches, add a gather marker - at a new line after the branches.

In the following example, regardless of whether the player heads to the garden or the kitchen, they return to their room. There, they are presented with the next choice.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"

*   [Head into the garden]
    The chirp of crickets greet you as you enter the garden.
*   [Move to the kitchen]
    A crackling fireplace illuminates the dark room.
-   A while later, you return to your room.
*   [Lay in bed]
*   [Sit at table]

";
let mut story = read_story_from_string(content).unwrap();
let mut story_other = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(buffer.len(), 2);
assert_eq!(&buffer[0].text, "The chirp of crickets greet you as you enter the garden.\n");
assert_eq!(&buffer[1].text, "A while later, you return to your room.\n");
buffer.clear();
story_other.resume(&mut buffer).unwrap();
story_other.make_choice(1).unwrap();
story_other.resume(&mut buffer).unwrap();
assert_eq!(buffer.len(), 2);
assert_eq!(&buffer[0].text, "A crackling fireplace illuminates the dark room.\n");
assert_eq!(&buffer[1].text, "A while later, you return to your room.\n");
}

Nested gather points

Gathers can be performed for any nested level of choices. Simply add the corresponding number of gather markers - below.

In this example, both inner choices 1.1 and 1.2 will gather at 1.1. Inner choices 2.1 and 2.2 at gather 2.1. Then finally, both outer choices 1 and 2 at gather point 1.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"

*   Choice 1
    * *     Choice 1.1
    * *     Choice 1.2
    - -     Gather 1.1
*   Choice 2
    * *     Choice 2.1
    * *     Choice 2.2
    - -     Gather 2.1
-   Gather 1

";
let mut story = read_story_from_string(content).unwrap();
let mut story_other = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
story.make_choice(1).unwrap();
story.resume(&mut buffer).unwrap();
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(buffer.len(), 4);
assert_eq!(&buffer[0].text, "Choice 2\n");
assert_eq!(&buffer[1].text, "Choice 2.1\n");
assert_eq!(&buffer[2].text, "Gather 2.1\n");
assert_eq!(&buffer[3].text, "Gather 1\n");
buffer.clear();
story_other.resume(&mut buffer).unwrap();
story_other.make_choice(0).unwrap();
story_other.resume(&mut buffer).unwrap();
story_other.make_choice(1).unwrap();
story_other.resume(&mut buffer).unwrap();
assert_eq!(buffer.len(), 4);
assert_eq!(&buffer[0].text, "Choice 1\n");
assert_eq!(&buffer[1].text, "Choice 1.2\n");
assert_eq!(&buffer[2].text, "Gather 1.1\n");
assert_eq!(&buffer[3].text, "Gather 1\n");
}

Preamble

The script is divided into a preamble and the story content. The preamble contains variable declarations, metadata and inclusions of other documents. The content comes afterwards and can refer to declarations in the preamble.

The end of the preamble in a script is marked by the first line of text or story content. This can be a divert to the introductory scene.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r#"

// Global story tags are declared in the preamble
# title: Inkling 
# author: Petter Johansson

// ... as are global variables
CONST name = "d'Artagnan"
VAR rank = "Capitaine"

// First line of story content comes here, which ends the preamble declaration
-> introduction 

=== introduction ===
I opened my notebook to a blank page, pen in hand.
"#;
let story = read_story_from_string(content).unwrap();
let tags = story.get_story_tags();
assert_eq!(&tags[0], "title: Inkling");
assert_eq!(&tags[1], "author: Petter Johansson");
assert!(story.get_variable("name").is_some());
assert!(story.get_variable("rank").is_some());
}

Variables

Throughout a story it can be useful to introduce variables, which can be declared and used in the story text. This makes it easy to keep a story consistent and track a story state.

Declaring variables

Global variables can be declared in the script using the VAR keyword. They must be declared in the preamble: before the first knot.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Variable};
let content = r#"

VAR a_float = 1.0
VAR a_int = 2
VAR a_bool = false
VAR a_string = "A String"
VAR a_destination = "-> stairwell"

"#;
let story = read_story_from_string(content).unwrap();
assert_eq!(story.get_variable("a_float").unwrap(), Variable::Float(1.0));
assert_eq!(story.get_variable("a_int").unwrap(), Variable::Int(2));
assert_eq!(story.get_variable("a_bool").unwrap(), Variable::Bool(false));
assert_eq!(story.get_variable("a_string").unwrap(), Variable::String("A String".to_string()));
assert!(story.get_variable("a_destination").is_some());
}

As shown in this example, the variable type is automatically assigned from the given value. Once assigned, a variable’s type cannot be changed.

Using variables in text

Variables can be inserted into text by enclosing them in curly braces.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Variable};
let content = r#"

VAR time = 11
VAR moon = "gloomy"

The time was {time}. A {moon} moon illuminated the room.

"#;
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(buffer[0].text, "The time was 11. A gloomy moon illuminated the room.\n");
}

Variable assignment

It is not currently possible to assign variables in the script. Use Story::set_variable.

Constant variables

Constant variables, whose values cannot be changed, are declared using the CONST keyword.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Variable};
let content = r#"

CONST name = "d'Artagnan" // Constant variable, cannot be modified
VAR rank = "Capitaine"    // Non-constant variable, can be changed

"#;
let mut story = read_story_from_string(content).unwrap();
assert_eq!(story.get_variable("name").unwrap(), Variable::from("d'Artagnan"));
assert!(story.set_variable("name", "Aramis").is_err());
}

Variable mathematics

Variable comparisons

Conditional content

Ink provides many methods for varying the text content of a line or the choices presented to a user.

Choice conditions

The easiest way to gate which choices are presented to the user is to check if they have visited a knot in the story. This is done by preceding the choice with the knot name enclosed by curly braces, for example {knot}.

In the following example, the first choice is only presented if the player has previously visited the knot with name tea_house.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Location, Prompt};
let content = r#"
-> choice
=== choice ===

+   {tea_house} "Yes, I saw them at 'Au thé à la menthe.'"
+   "No, I have not met them."

=== tea_house ===
-> choice
"#;
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 1);
      assert_eq!(choices[0].text, r#""No, I have not met them.""#);
  }
  _ => unreachable!()
}
story.move_to(&Location::from("tea_house")).unwrap();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 2);
      assert_eq!(choices[0].text, r#""Yes, I saw them at 'Au thé à la menthe.'""#);
  }
  _ => unreachable!()
}
}

Under the hood, inkling resolves this by translating {tea_house} as a variable whose value is the number of times the knot has been visited. It then asserts whether that value is “true”, which in Ink is whether it is non-zero. Thus, {tea_house} is an implicit form of writing the explicit condition {tea_house != 0}.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Location, Prompt};
let content = r#"
-> choice
=== choice ===

+   {tea_house != 0} "Yes, I saw them at 'Au thé à la menthe.'"
+   "No, I have not met them."

=== tea_house ===
-> choice
"#;
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 1);
      assert_eq!(choices[0].text, r#""No, I have not met them.""#);
  }
  _ => unreachable!()
}
story.move_to(&Location::from("tea_house")).unwrap();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 2);
      assert_eq!(choices[0].text, r#""Yes, I saw them at 'Au thé à la menthe.'""#);
  }
  _ => unreachable!()
}
}

Knowing this, we can of course also test these conditions using other types of variables.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"

VAR visited_château = true
VAR coins = 3

+   {visited_château} You recognize the bellboy.
+   {coins > 2} [Tip the bellboy]
+   {coins <= 2} [You cannot afford entry]

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 2);
      assert_eq!(choices[0].text, "You recognize the bellboy.");
      assert_eq!(choices[1].text, "Tip the bellboy");
  }
  _ => unreachable!()
}
}

Multiple conditions

Multiple conditions can be tested at once by supplying them one after another. All must be true for the choice to be presented. In this example, the first and third choices will be presented.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"

VAR visited_château = true
VAR coins = 6

+   {visited_château} {coins > 5} Purchase the painting
+   {not visited_château} {coins > 5} Your wallet itches but you see nothing of interest.
+   Leave the exhibit

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices.len(), 2);
      assert_eq!(choices[0].text, "Purchase the painting");
      assert_eq!(choices[1].text, "Leave the exhibit");
  }
  _ => unreachable!()
}
}

Beginning choices with variables instead of conditions

Finally, in case you want the choice text to begin with a variable, “escape” the first curly brace by prepending it with a \ character. This is so that inkling will know to write the variable as text, not evaluate it as a condition. This is an unfortunate quirk of the very compact Ink scripting language, where curly braces play many roles.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r#"

VAR mentor = "Evan"

+   \{mentor}, your mentor, greets you

"#;
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices[0].text, "Evan, your mentor, greets you");
  }
  _ => unreachable!()
}
}

For more information about which types of comparisons are supported, see the section on variable comparisons.

Text conditions

Conditions for displaying text are very similar to how conditions work for choices, but work on this format: {condition: process this if true | otherwise process this}. A colon : follows the condition, the content is inside the braces and an optional | marker marks content to show if the condition is not true.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"

VAR visited_château = false
VAR coins = 3

You {visited_château: recognize a painting | see nothing of interest}.
{coins < 5: You cannot afford anything.}

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer[0].text, "You see nothing of interest.\n");
assert_eq!(&buffer[1].text, "You cannot afford anything.\n");
}
You see nothing of interest.
You cannot afford anything.

Again, see the section on variable comparisons for more information about how conditions can be tested.

Nesting conditions

Conditions can naturally be nested inside of conditional content:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"

VAR met_evan = true
VAR met_austin = false

{met_evan: Yes, I met with Evan {met_austin: and | but not} Austin}.

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer[0].text, "Yes, I met with Evan but not Austin.\n");
}
Yes, I met with Evan but not Austin.

Diverts inside conditions

Content inside of conditions can divert to other knots.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r"

VAR met_evan = true

{met_evan: Evan takes you to his home. -> château | -> END }

=== château ===
The car ride takes a few hours.

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer[0].text, "Evan takes you to his home.\n");
assert_eq!(&buffer[1].text, "The car ride takes a few hours.\n");
}
Evan takes you to his home.
The car ride takes a few hours.

Alternating sequences

Ink comes with several methods which vary the content of a line every time it is seen. These are known as alternating sequences of content.

Sequences

Alternative sequences can be declared in a line using curly braces and | separators. Every time the line is revisited, the next piece will be presented, allowing us to write something like this:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"
-> continue
=== continue ===

The train had arrived {in Mannheim|in Heidelberg|at its final stop}.

+ [Continue] -> continue
";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "The train had arrived in Mannheim.\n");
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "The train had arrived in Heidelberg.\n");
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "The train had arrived at its final stop.\n");
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "The train had arrived at its final stop.\n");
}

When revisiting this line, it will go through the alternatives in order, then repeat the final value after reaching it. In Ink terms, this is called a sequence.

The train had arrived in Mannheim.
The train had arrived in Heidelberg.
The train had arrived at its final stop.
The train had arrived at its final stop.

Cycle sequences

Cycle sequences repeat the entire sequence after reaching the final piece. They are denoted by starting the first alternative with a & marker.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"
-> continue
=== continue ===

Today is a {&Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday}.

+ [Continue] -> continue
";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
for day in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", "Monday"].iter() {
  story.resume(&mut buffer).unwrap();
  assert_eq!(buffer.last().unwrap().text, format!("Today is a {}.\n", day));
  story.make_choice(0).unwrap();
}
}
Today is a Monday.
Today is a Tuesday.
Today is a Wednesday.
[...]
Today is a Sunday.
Today is a Monday.

Once-only sequences

Once-only sequences goes through all alternatives in order and then produce nothing. They are denoted by starting the first alternative with a ! marker.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"
-> continue
=== continue ===

I met with Anirudh{! for the first time| for the second time}.

+ [Continue] -> continue
";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "I met with Anirudh for the first time.\n");
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "I met with Anirudh for the second time.\n");
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "I met with Anirudh.\n");
}
I met with Anirudh for the first time.
I met with Anirudh for the second time.
I met with Anirudh.

Shuffle sequences

Shuffle sequences go through the alternatives in a random order, then shuffle the alternatives and deal them again. They are denoted by starting the first alternative with a ~ marker.

Note that these are only random if inkling has been compiled with the random feature. Otherwise they mimic the behavior of cycle sequences.


#![allow(unused)]
fn main() {
let content = r"
-> continue
=== continue ===

I was dealt a Jack of {~hearts|spades|diamonds|clubs}.

+ [Continue] -> continue
";
}
I was dealt a Jack of diamonds.
I was dealt a Jack of spades.
I was dealt a Jack of hearts.
I was dealt a Jack of clubs.

Nested alternatives

Alternatives can of course hide even more alternatives. How would we otherwise have any fun in life?


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"
-> continue
=== continue ===

I {&{strode|walked} hastily|waltzed {gracefully|clumsily}} into the room.

+ [Continue] -> continue
";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "I strode hastily into the room.\n");
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "I waltzed gracefully into the room.\n");
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "I walked hastily into the room.\n");
story.make_choice(0).unwrap();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, "I waltzed clumsily into the room.\n");
}
I strode hastily into the room.
I waltzed gracefully into the room.
I walked hastily into the room.
I waltzed clumsily into the room.

Diverts in alternatives

We can use diverts inside of alternatives to alternatively trigger different parts of the story.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"
-> continue
=== continue ===

The {first|next} time I saw the door it was {locked. -> locked_door|open. -> open_door}

=== locked_door ===
I had to return another day.
-> continue

=== open_door ===
In the doorway stood a thin figure.

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer[0].text, "The first time I saw the door it was locked.\n");
assert_eq!(&buffer[1].text, "I had to return another day.\n");
assert_eq!(&buffer[2].text, "The next time I saw the door it was open.\n");
assert_eq!(&buffer[3].text, "In the doorway stood a thin figure.\n");
}
The first time I saw the door it was locked. 
I had to return another day.
The next time I saw the door it was open.
In the doorway stood a thin figure.

Here’s where glue can come in handy. By adding glue before the divert we can continue the same paragraph in a new knot. Building on the previous example:


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r"
-> continue
=== continue ===

The {first|next} time I saw the door it was {locked. -> locked_door|open. -> open_door}

=== locked_door ===
<> I had to return another day.
-> continue

=== open_door ===
<> In the doorway stood a thin figure.

";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer[0].text, "The first time I saw the door it was locked. ");
assert_eq!(&buffer[1].text, "I had to return another day.\n");
assert_eq!(&buffer[2].text, "The next time I saw the door it was open. ");
assert_eq!(&buffer[3].text, "In the doorway stood a thin figure.\n");
}
The first time I saw the door it was locked. I had to return another day.
The next time I saw the door it was open. In the doorway stood a thin figure.

Story metadata

These features do not impact the story flow but contain additional information.

Tags

Information about the story, knots or even individual lines can be marked with tags. All tags begin with the # marker.

Tags are stored as pure strings and can thus be of any form you want. inkling assigns no meaning to them on its own, it’s for you as the user to decide how to treat them.

Global tags

Tags in the preamble are global story tags. Here you can typically mark up metadata for the script.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Variable};
let content = r#"

# title: Inkling
# author: Petter Johansson

"#;
let story = read_story_from_string(content).unwrap();
let tags = story.get_story_tags();
assert_eq!(&tags[0], "title: Inkling");
assert_eq!(&tags[1], "author: Petter Johansson");
}

Knot tags

Tags encountered in a knot before any content is parsed as tags belonging to that knot.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r#"

=== stairwell ===
# sound: blowing_wind.ogg
# dark, quiet, dangerous
I made my way down the empty stairwell.

"#;
let story = read_story_from_string(content).unwrap();
let tags = story.get_knot_tags("stairwell").unwrap();
assert_eq!(&tags[0], "sound: blowing_wind.ogg");
assert_eq!(&tags[1], "dark, quiet, dangerous");
}

Line tags

Lines can be tagged by adding the tag after the line content. Multiple tags can be set, separated by additional ‘#’ markers.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r#"

A pale moonlight illuminated the garden. # sound: crickets.ogg
The well stank of stagnant water. # smell, fall # sound: water_drip.ogg

"#;
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
let tags1 = &buffer[0].tags;
let tags2 = &buffer[1].tags;
assert_eq!(&tags1[0], "sound: crickets.ogg");
assert_eq!(&tags2[0], "smell, fall");
assert_eq!(&tags2[1], "sound: water_drip.ogg");
}

Tags can also be added to choice lines.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::{read_story_from_string, Prompt};
let content = r#"

*   I made my way to the well. # sound: footsteps.ogg

"#;
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
match story.resume(&mut buffer).unwrap() {
  Prompt::Choice(choices) => {
      assert_eq!(choices[0].tags[0], "sound: footsteps.ogg");
  }
  _ => unreachable!()
}
}

To-do comments

To-do comments are lines which start with TODO:, including the colon. When the script is parsed, these comments are removed from the text and added to the log as reminders.


#![allow(unused)]
fn main() {
extern crate inkling;
use inkling::read_story_from_string;
let content = r#"
-> fireworks

=== fireworks ===
TODO: Make this more snappy.
Emtithal woke up to the sound of fireworks.

"#;
let mut story = read_story_from_string(content).unwrap();
assert_eq!(story.log.todo_comments.len(), 1);
let mut buffer = Vec::new();
story.resume(&mut buffer).unwrap();
assert_eq!(&buffer[0].text, "Emtithal woke up to the sound of fireworks.\n");
}

Missing features

This page lists notable features of Ink which are currently missing in inkling. Some may be implemented, others will be more difficult.

Variable assignment

Assigning new values to variables in the script.

~ rank = "Capitaine"
~ coins = coins + 4

Including other files

Dividing the script into several files and including them in the preamble of the main script.

INCLUDE château.ink
INCLUDE gloomwood.ink

Multiline comments

Using /* and */ markers to begin and end multiline comments.

Line one /* We can use multiline comments
            to split them over several lines, 
            which may aid readability. */
Line two

Multiline conditionals

Using multiline blocks to create larger if-else or switch statements.

{condition:
    if condition content
- else:
    else this content
}
{
    - condition1:
        if condition 1 content
    - condition2:
        else if condition 2 content
    - else:
        else this content
}

Labels

Add labels to choices and gather points to refer and divert to them.

*   (one) Choice
*   (two) Choice
-   (gather) Gather 

Functions

Calling various types of functions from the script.

Built-in functions

Support for the pre-defined functions of Ink.

~ result = RANDOM(1, 6)
~ result = POW(3, 2)

Definining functions

Defining functions in the script to print text, modify variables and return calculations.

// Modifying referenced values
=== function add(ref x, value) ===
~ x = x + value
// Modifying global variables
VAR coins = 0
=== function add_wealth(v) ===
~ coins = coins + v
// Writing text 
=== function greet(person) ===
Greetings, {person}!

External functions

Begin able to call external Rust functions from the script.

Threads

More information.

Tunnels

More information.

Advanced state tracking

More information.

In regards to compatibility

The authors have made a best effort attempt to replicate the behavior of Ink in most respects. However, since this is a completely separate implementation, there are surely certain features which even if implemented (and many aren’t), result in different output from the same input .ink script. Edge cases may be a-plenty.

We do care about making Ink and inkling as similar as possible, to the extent that we can. If you find cases where the results differ, please let us know by opening an issue on the Github repository.

In the end, inkling cannot be considered a drop-in replacement for Inkle’s own implementation of the language. More realistically, it’s inspired by it, sharing most features but with results which may differ. Keep this in mind while writing a script.

I’d like to end this note by thanking Inkle for designing the language and being the inspiration for this project. And of course for their many fantastic games and stories, which is some of the best work out there.

— Petter Johansson, 2020