Utiliser Loreline avec C++

Loreline fournit une bibliothèque C++ avec des binaires pré-compilés pour Mac, Linux et Windows. L'API utilise des fonctions de style C avec un seul fichier d'en-tête, ne nécessitant que C++11. Ce guide montre comment configurer un projet, charger un script .lor, et gérer les dialogues, les choix et la fin du script.

Installer la bibliothèque

Téléchargez loreline-cpp.zip (v0.9.1). L'archive contient :

L'archive peut servir de point de départ pour votre propre projet, ou de référence pour intégrer Loreline dans un projet existant.

Configuration du projet avec CMake

Pour ajouter Loreline à un projet CMake existant, pointez vers les répertoires include et lib :

target_include_directories(votre_app PRIVATE /chemin/vers/loreline/include)
target_link_directories(votre_app PRIVATE /chemin/vers/loreline/mac)  # ou linux-x86_64, windows
target_link_libraries(votre_app PRIVATE Loreline)

Assurez-vous que la bibliothèque partagée (libLoreline.dylib, libLoreline.so ou Loreline.dll) se trouve à côté de votre exécutable au lancement. Sur Mac et Linux, configurez le rpath :

# Mac
set_target_properties(votre_app PROPERTIES BUILD_RPATH "@executable_path")
# Linux
set_target_properties(votre_app PROPERTIES BUILD_RPATH "$ORIGIN")

Sur Windows, copiez la DLL à côté de votre exécutable en étape post-build.

Initialisation et nettoyage

Avant d'utiliser toute fonction Loreline, appelez Loreline_init(). Une fois terminé, appelez Loreline_dispose() :

#include "Loreline.h"

int main() {
    Loreline_init();

    // ... utiliser Loreline ...

    Loreline_dispose();
    return 0;
}

Charger un script

Utilisez Loreline_parse() pour parser une chaîne de caractères issue d'un fichier .lor. Le troisième argument est un callback de fichier pour résoudre les instructions import :

#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
);

Le callback de fichier doit appeler Loreline_provideFile(request, content) exactement une fois. Vous pouvez l'appeler de manière synchrone dans le callback, ou plus tard depuis n'importe où dans votre code. Passez un Loreline_String() vide pour signaler « fichier non trouvé ». L'appel à Loreline_parse() ne se termine pas tant que chaque demande de fichier n'a pas reçu une réponse.

Si votre script n'a pas d'instructions import, vous pouvez passer NULL pour le callback de fichier :

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

Gérer les dialogues

Le callback de dialogue est un pointeur de fonction C. Il reçoit l'interpréteur, un identifiant de personnage (utilisez isNull() pour vérifier s'il s'agit de texte narratif), le texte du dialogue, les tags et une fonction advance à appeler pour continuer :

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()) {
        // Résoudre le nom d'affichage depuis la définition du personnage
        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 {
        // Texte narratif
        printf("%s\n", text.c_str());
    }
    advance();
}

Loreline_String est un type de chaîne de caractères à comptage de références, vous n'avez jamais besoin de la libérer manuellement. Utilisez .c_str() pour obtenir une chaîne C terminée par null, et .isNull() pour vérifier s'il s'agit d'une valeur nulle.

Gérer les choix

Le callback de choix reçoit un tableau de Loreline_ChoiceOption. Chaque option a un champ text (un Loreline_String) et un champ enabled (un bool). Appelez select(index) avec l'index (base 0) de l'option choisie :

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());
        }
    }

    // Lire l'entrée du joueur
    printf("> ");
    fflush(stdout);
    char buf[64];
    if (fgets(buf, sizeof(buf), stdin)) {
        int choice = atoi(buf);
        if (choice >= 1 && choice <= optionCount) {
            select(choice - 1);
        }
    }
}

Gérer la fin du script

Le callback de fin est appelé quand le script atteint sa fin :

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

Démarrer depuis un beat spécifique

Par défaut, Loreline_play() démarre depuis le début du script. Pour démarrer depuis un beat spécifique, passez son nom :

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

Options de l'interpréteur

Vous pouvez passer des options supplémentaires à Loreline_play() pour enregistrer des fonctions personnalisées ou appliquer des traductions :

// Fonction personnalisée
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);

// Traductions : charge tous les fichiers `.fr.lor` à travers les imports du script.
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() parcourt l'arbre des imports du script et charge le fichier .<lang>.lor correspondant à côté de chaque fichier importé. Les fichiers de traduction absents sont silencieusement ignorés.

Durée de vie de userData

Les signatures de Loreline_play() et Loreline_resume() acceptent deux arguments optionnels de type pointeur sur fonction : retain et release. Ils valent NULL par défaut, et tous les exemples de ce guide les omettent.

Quand les utiliser : Presque jamais. Du code C++ classique qui gère userData directement, comme un pointeur brut vers un objet de pile ou un objet alloué avec new que vous libérez après la fin de l'interpréteur, peut laisser les valeurs par défaut.

Quand ils sont utiles : Lorsque userData pointe vers une ressource comptée par référence (par exemple un shared_ptr déballé en pointeur brut, ou un objet de liaison de langage comme un wrapper RefCounted Godot). Loreline met les callbacks en file d'attente interne entre les frames ; l'objet derrière userData doit rester alloué entre le moment où Loreline place un callback en file et le moment où le callback est délivré. Si votre code pouvait relâcher sa dernière référence pendant cette fenêtre, fournissez ces hooks et Loreline gardera une référence supplémentaire pour vous :

La GDExtension Godot fournie avec Loreline utilise ce système. Voir godot/src/loreline_interpreter.cpp pour une référence fonctionnelle.

Fonctions personnalisées asynchrones

Les fonctions personnalisées enregistrées avec Loreline_optionsAddFunction() doivent retourner une valeur de manière synchrone. Si vous avez besoin d'attendre une opération asynchrone (un appel réseau, un minuteur, un signal) avant que le script continue, utilisez plutôt Loreline_optionsAddAsyncFunction(). Le script appelle la fonction par son nom comme n'importe quelle autre :

beat start
  Récupération de votre score...
  fetchScore()
  Votre score est de $score.

L'hôte enregistre un callback asynchrone et, à l'intérieur, appelle Loreline_resolveAsync(resolve, ...) lorsque le travail se termine, ou Loreline_cancelAsync(resolve) pour abandonner sans reprendre l'interpréteur. Exactement l'un des deux doit être appelé ; les deux libèrent le handle de résolution.

void fetchScoreAsync(
    Loreline_Interpreter* interp,
    const Loreline_Value* args, int argCount,
    Loreline_AsyncResolve* resolve,
    void* userData
) {
    // Démarre le travail côté hôte. À la fin (depuis n'importe quel thread), appeler :
    //   Loreline_resolveAsync(resolve, Loreline_Value::null_val());
    // Si l'hôte abandonne avant la fin, appeler :
    //   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);

Les fonctions asynchrones ne peuvent être appelées qu'entre plusieurs dialogues, avant ou après un choix. Elles ne peuvent pas apparaître dans des expressions ou des interpolations de chaînes de caractères.

Sauvegarder et restaurer l'état

Appelez Loreline_save() à tout moment pour capturer l'état courant de l'interpréteur sous forme de chaîne JSON, et Loreline_resume() plus tard pour créer un nouvel interpréteur qui reprend depuis le point sauvegardé :

// Sauvegarder, typiquement quand le joueur quitte ou après chaque dialogue.
Loreline_String saveData = Loreline_save(interp);
std::string savedStr(saveData.c_str()); // persister sur disque, en réseau, etc.

// Reprendre au prochain lancement en recréant l'interpréteur depuis la chaîne sauvegardée.
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() accepte les mêmes paramètres que Loreline_play() plus une chaîne de données sauvegardées et un nom de beat (passez un Loreline_String() vide pour reprendre depuis le beat sauvegardé).

Timing. Une sauvegarde prise pendant un callback de dialogue ou de choix capture l'état de sorte que la reprise re-délivre ce même callback. Le joueur revoit la même ligne (ou les mêmes choix), et avancer depuis là continue normalement. C'est habituellement ce que vous voulez pour une sauvegarde automatique après chaque dialogue : le joueur reprend exactement où il en était.

Exemple complet

Voici une application console complète qui charge et exécute un script Loreline :

#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("--- Fin ---\n");
}

int main() {
    std::string content = readFile("story/CoffeeShop.lor");
    if (content.empty()) {
        fprintf(stderr, "Erreur : impossible de lire le fichier\n");
        return 1;
    }

    Loreline_init();

    Loreline_Script* script = Loreline_parse(
        content.c_str(), "story/CoffeeShop.lor", onFileRequest, NULL
    );
    if (!script) {
        fprintf(stderr, "Erreur : échec de l'analyse du 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;
}

Aller plus loin

Le téléchargement loreline-cpp.zip inclut des scripts de build par plateforme (build-mac.sh, build-linux.sh, build-windows.bat) et un exemple fonctionnel complet avec une histoire utilisant des définitions de personnages et des imports.