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:
loreline/include/Loreline.h: the public headerloreline/mac/libLoreline.dylib: pre-built macOS libraryloreline/linux-arm64/libLoreline.soandloreline/linux-x86_64/libLoreline.so: pre-built Linux librariesloreline/windows/Loreline.dllandloreline/windows/Loreline.lib: pre-built Windows libraryCMakeLists.txt: ready-to-use CMake projectmain.cpp: complete sample applicationstory/: sample.lorfiles
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:
retain(userData)is called before each callback is queued. Return any handle that the matchingreleasecan use to drop the reference.release(handle)is called after the queued callback has completed. Drop the reference here.
The Godot GDExtension bundled with Loreline uses this pattern. See
godot/src/loreline_interpreter.cppfor 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.