Using Loreline with C++

Loreline provides a C++ library with pre-built binaries for macOS, Linux, and Windows. The API uses C-style functions with a single header file, requiring only C++11. This guide shows how to set up a project, load a .lor script, and handle dialogue, choices, and script completion.

Installing the library

Download loreline-cpp.zip (v0.9.0). The archive contains:

The archive can be used as a starting point for your own project, or as a reference for integrating Loreline into an existing one.

Project setup with CMake

To add Loreline to an existing CMake project, point to the include and lib directories:

target_include_directories(your_app PRIVATE /path/to/loreline/include)
target_link_directories(your_app PRIVATE /path/to/loreline/mac)  # or linux-x86_64, windows
target_link_libraries(your_app PRIVATE Loreline)

Make sure the shared library (libLoreline.dylib, libLoreline.so, or Loreline.dll) is next to your executable at runtime. On macOS and Linux, set the rpath:

# macOS
set_target_properties(your_app PROPERTIES BUILD_RPATH "@executable_path")
# Linux
set_target_properties(your_app PROPERTIES BUILD_RPATH "$ORIGIN")

On Windows, copy the DLL next to your executable as a post-build step.

Initialization and cleanup

Before using any Loreline function, call Loreline_init(). When you are done, call Loreline_dispose():

#include "Loreline.h"

int main() {
    Loreline_init();

    // ... use Loreline ...

    Loreline_dispose();
    return 0;
}

Loading a script

Use Loreline_parse() to parse a .lor string. The third argument is a file handler for resolving import statements:

#include <fstream>
#include <sstream>
#include <string>

std::string readFile(const std::string& path) {
    std::ifstream f(path, std::ios::binary);
    if (!f.is_open()) return std::string();
    std::ostringstream ss;
    ss << f.rdbuf();
    return ss.str();
}

void onFileRequest(
    Loreline_String path,
    Loreline_FileRequest* request,
    void* userData
) {
    std::string content = readFile(path.c_str());
    Loreline_provideFile(request,
        content.empty() ? Loreline_String() : Loreline_String(content.c_str()));
}

std::string content = readFile("story/CoffeeShop.lor");
Loreline_Script* script = Loreline_parse(
    content.c_str(), "story/CoffeeShop.lor", onFileRequest, NULL
);

The file handler must call Loreline_provideFile(request, content) exactly once. You can call it synchronously inside the handler, or later from anywhere in your code. Pass an empty Loreline_String() to signal "not found". The script's Loreline_parse() call won't complete until every file request has been answered.

If your script has no import statements, you can pass NULL for the file handler:

Loreline_Script* script = Loreline_parse(content.c_str(), "CoffeeShop.lor", NULL, NULL);

Handling dialogue

The dialogue handler is a C function pointer. It receives the interpreter, a character identifier (use isNull() to check for narrative text), the dialogue text, tags, and an advance function to call when ready to continue:

void onDialogue(
    Loreline_Interpreter* interp,
    Loreline_String character,
    Loreline_String text,
    const Loreline_TextTag* tags,
    int tagCount,
    void (*advance)(void),
    void* userData
) {
    if (!character.isNull()) {
        // Resolve display name from character definition
        Loreline_Value nameVal = Loreline_getCharacterField(interp, character, "name");
        const char* displayName = (nameVal.type == Loreline_StringValue && nameVal.stringValue)
            ? nameVal.stringValue.c_str()
            : character.c_str();
        printf("%s: %s\n", displayName, text.c_str());
    } else {
        // Narrative text
        printf("%s\n", text.c_str());
    }
    advance();
}

Loreline_String is a ref-counted string type, so you never need to free it manually. Use .c_str() to get a null-terminated C string, and .isNull() to check if it represents a null value.

Handling choices

The choice handler receives an array of Loreline_ChoiceOption. Each option has text (a Loreline_String) and enabled (a bool). Call select(index) with the 0-based index of the chosen option:

void onChoice(
    Loreline_Interpreter* interp,
    const Loreline_ChoiceOption* options,
    int optionCount,
    void (*select)(int index),
    void* userData
) {
    for (int i = 0; i < optionCount; i++) {
        if (options[i].enabled) {
            printf("  [%d] %s\n", i + 1, options[i].text.c_str());
        }
    }

    // Read player input
    printf("> ");
    fflush(stdout);
    char buf[64];
    if (fgets(buf, sizeof(buf), stdin)) {
        int choice = atoi(buf);
        if (choice >= 1 && choice <= optionCount) {
            select(choice - 1);
        }
    }
}

Handling script completion

The finish handler is called when the script reaches its end:

void onFinish(Loreline_Interpreter* interp, void* userData) {
    printf("--- The End ---\n");
}

Starting from a specific beat

By default, Loreline_play() starts from the beginning of the script. To start from a specific beat, pass its name:

Loreline_play(script, onDialogue, onChoice, onFinish, "MorningScene");

Interpreter options

You can pass additional options to Loreline_play() to register custom functions or apply translations:

// Custom function
Loreline_Value rollFn(Loreline_Interpreter* interp, const Loreline_Value* args, int argCount, void* userData) {
    return Loreline_Value::from_int(rand() % args[0].intValue + 1);
}

Loreline_InterpreterOptions* opts = Loreline_createOptions();
Loreline_optionsAddFunction(opts, "roll", rollFn, NULL);

// Translations: load every `.fr.lor` file across the script's imports.
Loreline_Translations* translations = Loreline_loadLocale(
    "fr", script, "story/CoffeeShop.lor", onFileRequest, NULL);
Loreline_optionsSetTranslations(opts, translations);

Loreline_play(script, onDialogue, onChoice, onFinish, Loreline_String(), opts);

Loreline_releaseOptions(opts);
Loreline_releaseTranslations(translations);

Loreline_loadLocale() walks the script's import tree and loads the matching .<lang>.lor file next to each imported file. Missing translation files are skipped silently.

userData lifetime hooks

The Loreline_play() and Loreline_resume() signatures accept two optional function-pointer arguments: retain and release. They default to NULL, and every example in this guide omits them.

When to use these: Almost never. Plain C++ code that manages userData directly, such as a raw pointer to a stack object or a new'd heap object you release after the interpreter finishes, should leave these defaults.

When they're useful: When userData points at a reference-counted resource (for example a shared_ptr unwrapped into a raw pointer, or a language-binding object such as a Godot RefCounted wrapper). Loreline queues callbacks internally between frames; the object backing userData must stay alive from the moment Loreline queues a callback until the callback has fired. If your code could drop its last reference in that window, provide these hooks and Loreline will hold an additional reference for you:

The Godot GDExtension bundled with Loreline uses this pattern. See godot/src/loreline_interpreter.cpp for a working reference.

Async custom functions

Custom functions registered with Loreline_optionsAddFunction() must return a value synchronously. If you need to wait on an asynchronous operation (a network call, a timer, a signal) before the script continues, use Loreline_optionsAddAsyncFunction() instead. The script calls the function by name like any other:

beat start
  Fetching your score...
  fetchScore()
  Your score is $score.

The host registers an async callback and, inside it, calls Loreline_resolveAsync(resolve, ...) when the work completes, or Loreline_cancelAsync(resolve) to abort without resuming the interpreter. Exactly one of the two must be called; both release the resolve handle.

void fetchScoreAsync(
    Loreline_Interpreter* interp,
    const Loreline_Value* args, int argCount,
    Loreline_AsyncResolve* resolve,
    void* userData
) {
    // Kick off host-side work. When it completes (from any thread), call:
    //   Loreline_resolveAsync(resolve, Loreline_Value::null_val());
    // If the host aborts before completion, call:
    //   Loreline_cancelAsync(resolve);
    myAsyncWorker.enqueue([interp, resolve]() {
        Loreline_setTopLevelStateField(
            interp, "score", Loreline_Value::from_int(42));
        Loreline_resolveAsync(resolve, Loreline_Value::null_val());
    });
}

Loreline_InterpreterOptions* opts = Loreline_createOptions();
Loreline_optionsAddAsyncFunction(opts, "fetchScore", fetchScoreAsync, NULL);

Loreline_play(script, onDialogue, onChoice, onFinish, Loreline_String(), opts);

Async functions can only be called between dialogues, or before and after a choice. They cannot appear inside expressions or string interpolations.

Saving and restoring state

Call Loreline_save() at any time to capture the interpreter's current state as a JSON string, and Loreline_resume() later to create a fresh interpreter that continues from the saved point:

// Save, typically when the player quits or after each dialogue.
Loreline_String saveData = Loreline_save(interp);
std::string savedStr(saveData.c_str()); // persist to disk, network, etc.

// Resume on next launch by recreating the interpreter from the saved string.
Loreline_Script* script = Loreline_parse(content.c_str(), "story.lor", NULL, NULL);
Loreline_Interpreter* resumed = Loreline_resume(
    script, onDialogue, onChoice, onFinish, savedStr.c_str());

Loreline_resume() accepts the same parameters as Loreline_play() plus a save-data string and a beat name (pass an empty Loreline_String() to resume from the saved beat).

Timing. A save taken during a dialogue or choice callback captures the state such that resuming re-delivers that same callback. The player sees the same line (or the same choices) again, and advancing from there continues normally. This is usually what you want for auto-save after each dialogue: the player resumes exactly where they were.

Complete example

Here is a complete console application that loads and plays a Loreline script:

#include "Loreline.h"
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <string>

static std::string readFile(const std::string& path) {
    std::ifstream f(path, std::ios::binary);
    if (!f.is_open()) return std::string();
    std::ostringstream ss;
    ss << f.rdbuf();
    return ss.str();
}

static void onFileRequest(
    Loreline_String path, Loreline_FileRequest* request, void*
) {
    std::string content = readFile(path.c_str());
    Loreline_provideFile(request,
        content.empty() ? Loreline_String() : Loreline_String(content.c_str()));
}

static void onDialogue(
    Loreline_Interpreter* interp, Loreline_String character,
    Loreline_String text, const Loreline_TextTag*, int,
    void (*advance)(void), void*
) {
    if (!character.isNull()) {
        Loreline_Value nameVal = Loreline_getCharacterField(interp, character, "name");
        const char* name = (nameVal.type == Loreline_StringValue && nameVal.stringValue)
            ? nameVal.stringValue.c_str() : character.c_str();
        printf("%s: %s\n\n", name, text.c_str());
    } else {
        printf("%s\n\n", text.c_str());
    }
    advance();
}

static void onChoice(
    Loreline_Interpreter*, const Loreline_ChoiceOption* options,
    int optionCount, void (*select)(int), void*
) {
    for (int i = 0; i < optionCount; i++) {
        if (options[i].enabled)
            printf("  [%d] %s\n", i + 1, options[i].text.c_str());
    }
    printf("> ");
    fflush(stdout);
    char buf[64];
    if (fgets(buf, sizeof(buf), stdin)) {
        int choice = atoi(buf);
        if (choice >= 1 && choice <= optionCount)
            select(choice - 1);
    }
}

static void onFinish(Loreline_Interpreter*, void*) {
    printf("--- The End ---\n");
}

int main() {
    std::string content = readFile("story/CoffeeShop.lor");
    if (content.empty()) {
        fprintf(stderr, "Error: cannot read story file\n");
        return 1;
    }

    Loreline_init();

    Loreline_Script* script = Loreline_parse(
        content.c_str(), "story/CoffeeShop.lor", onFileRequest, NULL
    );
    if (!script) {
        fprintf(stderr, "Error: failed to parse script\n");
        Loreline_dispose();
        return 1;
    }

    Loreline_Interpreter* interp = Loreline_play(
        script, onDialogue, onChoice, onFinish
    );

    if (interp) Loreline_releaseInterpreter(interp);
    Loreline_releaseScript(script);
    Loreline_dispose();
    return 0;
}

Going further

The loreline-cpp.zip download includes platform-specific build scripts (build-mac.sh, build-linux.sh, build-windows.bat) and a complete working sample with a story that uses character definitions and imports.