moteur de visual novel en Rust / SDL3
  • Rust 99.4%
  • Shell 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-27 13:56:22 +01:00
.cargo Build : trois modes SDL3 (statique par défaut / partagée / système) 2026-09-22 00:43:20 +02:00
.forgejo [Chiffrement] - Clé lue depuis .forgejo/arckey.txt 2026-10-27 13:42:08 +01:00
DOC [Chiffrement] - Clé lue depuis .forgejo/arckey.txt 2026-10-27 13:42:08 +01:00
game [Git] - Fins de ligne LF via .gitattributes 2026-10-27 13:56:22 +01:00
src [Chiffrement] - Clé lue depuis .forgejo/arckey.txt 2026-10-27 13:42:08 +01:00
tools [Archive] - Les fichier peuvent aussi être archivé 2026-10-12 09:35:51 +02:00
.gitattributes [Git] - Fins de ligne LF via .gitattributes 2026-10-27 13:56:22 +01:00
.gitignore [Archive] - Les fichier peuvent aussi être archivé 2026-10-12 09:35:51 +02:00
AGENTS.md [Chiffrement] - Clé lue depuis .forgejo/arckey.txt 2026-10-27 13:42:08 +01:00
Cargo.lock Initialisation : moteur vnth (Rust/SDL3, scripts .thn) 2026-09-22 00:27:03 +02:00
Cargo.toml Build : trois modes SDL3 (statique par défaut / partagée / système) 2026-09-22 00:43:20 +02:00
INSTALL.md Ajout INSTALL.md : compilation et récupération du binaire (Linux + Windows) 2026-09-23 10:01:02 +02:00
prepare-rt.sh Build : trois modes SDL3 (statique par défaut / partagée / système) 2026-09-22 00:43:20 +02:00
README.md [Chiffrement] - Clé lue depuis .forgejo/arckey.txt 2026-10-27 13:42:08 +01:00

vnth — moteur de visual novel en Rust / SDL3

Un petit moteur de visual novel novel-style (NVL) en Rust, utilisant SDL3 (rendu, audio, polices via SDL_ttf) et un format de script texte .thn.

Fonctionnalités

  • Scripts .thn : dialogue, étiquettes, variables, conditionnels, choix, appels de sous-protocoles.
  • Rendu NVL : texte tapé automatiquement, défilement, choix cliquables (flèches ↑/↓ + Entrée).
  • Images : fonds (@bg) et sprites (@sprite), redimensionnement cover.
  • Audio : musique (@music file loop|once), effets (@sfx file), volumes.
  • Vidéo : @video file (via ffmpeg/ffprobe, décodé en BGRA).
  • Effets : @fade de transition, @window WxH pour changer la résolution.
  • Sauvegardes : variables globales (save/global.sav), slots (save/slotN.sav), rapides (F5 / F9), @save / @load.
  • Plein écran (F11), Échap pour passer une vidéo.

Compilation

Crates : Rust ≥ 1.85, cargo, cmake, ninja, un compilateur C, et les en-têtes de développement SDL3 compilés from source (aucun binutils SDL3 système requis).

Trois modes de montage de SDL3 sont proposés via les features de vnth :

Mode Feature Résultat
Autonome (défaut) sdl-bundled-static SDL3 + SDL3_ttf compilés statiquement et embarqués dans le binaire (vendored). Les backends SDL restent dlopen au runtime (X11, Wayland…), mais aucune .so SDL à distribuer.
Package sdl-bundled-shared SDL3/SDL3_ttf compilés from-source en .so, placés à côté du binaire (<target>/rt/lib, rpath $ORIGIN/../rt/lib).
Système sdl-system SDL3/SDL3_ttf du système via pkg-config (nécessite SDL3-devel + SDL3_ttf-devel). Binaire plus petit.
cargo build                                      # mode autonome (défaut)
cargo build --no-default-features --features sdl-bundled-shared && ./prepare-rt.sh
cargo build --no-default-features --features sdl-system
cargo run -- --help

Le mode autonome compile SDL3 statique une première fois (~10 min), des builds ultérieurs sont incrémentaux. Un cargo clean ne supprime pas les dépendances compilées (target-dir séparé) mais vide le cache build-script — en mode partagé seul, relancer ./prepare-rt.sh après nettoyage.

Particularités de cette machine (sans sudo)

  1. Espace dans le chemin du projet. Le répertoire « projet vnth » contient un espace, ce qui fait échouer l'analyse des directives cargo::rustc-link-search… émises par les build scripts de sdl3-sys/sdl3-ttf-sys (elles sont tronquées à la première espace). Un répertoire cible sans espace est donc déclaré dans .cargo/config.toml :

    [build]
    target-dir = "/home/rivenraider/vnth-target"
    
  2. En-tête XScrnSaver manquant. La compilation from source de SDL3 échoue si X11/XScrnSaver.h n'est pas installé : CMake Error … Couldn't find dependency package for XSCRNSAVER. Correctif (une seule fois, dans la copie du registre cargo) :

    # dans sdl3-src-3.4.16/SDL/CMakeLists.txt
    # remplacer par OFF :
    dep_option(SDL_X11_XSCRNSAVER "Enable Xscrnsaver support" OFF SDL_X11 OFF)
    
  3. SDL3/SDL3_ttf partagés (mode « package »). Les bibliothèques compilées par les build scripts vivent dans <target>/debug/build/<crate>-*/out/lib64. ./prepare-rt.sh les copie vers <target>/rt/lib (le binaire les retrouve via son rpath). Aucun LD_LIBRARY_PATH n'est nécessaire.

  4. Audio. Sur une machine sans backend audio (ni PipeWire/Pulse, ni ALSA), l'audio est désactivé silencieusement ; le jeu continue (ruche dummy/disk de SDL). La musique et les effets fonctionneront sur un système avec un périphérique de sortie.

Utilisation

vnth [--script system/first.thn] [--label start] [--window 800x600] [--saves save/]
Touche Action
Espace / Entrée / clic gauche avancer (termine la frappe, valide le dialogue, choisit)
clic droit menu @rmenu (ou journal si @rlookback)
↑ / ↓ choisir une option / naviguer dans un menu, le sélecteur de slots ou le journal
PageUp / PageDown pages suivantes/précédentes (slots, journal)
S activer/désactiver le skip
A activer/désactiver le mode auto
F5 sauvegarde rapide (slot 0)
F9 chargement rapide (slot 0)
F11 plein écran
Échap fermer un menu / le journal, passer la vidéo en cours

Script .thn

  • ; au début d'une ligne : commentaire.
  • Ligne libre : dialogue (texte du script), affiché en NVL et tapé au fur et à mesure ; [var], %n et $n (variables indexées) sont interpolés ; #rrggbb colore la suite du texte ; \n force un retour à la ligne ; @ attend un clic (même page), \ attend un clic puis efface la page (_@/_\ au début annulent l'attente).
  • #label ou *label : étiquette (point de saut).
  • > texte -> label : option de menu, affichée par la commande @choice (les lignes > qui suivent @choice forment le menu). Un [condition] avant -> désactive l'option quand la condition est fausse ; une option sans -> poursuit simplement après le menu.
  • @commande : commande.
  • .tho : script chiffré (Rozier, clé = texte de .forgejo/arckey.txt). Le moteur le déchiffre à la volée ; si un .thn homonyme existe, il a toujours priorité.

Commandes

@bg fichier            ; fond (couverture)
@sprite nom fichier    ; pose un sprite
@move nom x y          ; déplace un sprite
@vsp nom [on|off]      ; masque/affiche un sprite sans le détruire
@hide nom              ; retire un sprite
@clear                 ; efface textes et sprites
@music fichier loop    ; musique (loop|once, défaut loop)
@music stop            ; coupe la musique
@sfx fichier           ; effet sonore
@musicvol n / @bgmvol n / @mp3vol n ; volume musique (0.0 → 1.0)
@sfxvol n / @sevol n / @voicevol n  ; volume effets (0.0 → 1.0)
@video fichier         ; lance une vidéo (alias @movie, @avi, @mpegplay)
@fade ms               ; fondu de transition
@window LxH            ; change la résolution logique
@delay ms              ; attend un délai (alias @wait)
@set $var = valeur     ; nombre, chaîne "…" ou booléen
@if cond               ; conditionnel (non imbriqué)
@elseif cond / @else / @endif
@jump label / @goto label ; saut local
@call label / @gosub label ; appelle une étiquette (retour via @return)
@return                ; revient de l'appel
@rnd var max           ; var = entier aléatoire dans [0, max)
@inc var / @dec var    ; incrémente / décrémente une variable
@choice                ; affiche le menu des lignes `> … -> …` suivantes
@select t1,l1 t2,l2 …  ; choix programmatiques (paires texte, étiquette —
                        ; virgules ou espaces acceptés, texte avec espaces cité)
@save [slot]           ; sauvegarde (défaut slot 1)
@load [slot]           ; charge
@quit                  ; quitte le jeu

Systémique (menu clic-droit, journal, skip, auto, sauvegardes)

@rmenu "Étiquette",action, …  ; menu clic droit ; actions : save, load,
                              ; lookback, close, skip, automode, quit
@rlookback                    ; le clic droit ouvre directement le journal
@lookbackon / @lookbackoff    ; active/désactive la collecte du journal
@maxkaisoupage n              ; borne le nombre de pages mémorisées
@skip / @skipoff              ; skip : s'arrête sur les choix et, avec
                              ; @kidokuskip, sur les lignes non lues
@kidokuskip [on|off]          ; skip uniquement sur le déjà lu
@automode [on|off]            ; avancement automatique après chaque attente
@automode_time ms             ; délai du mode auto
@savenumbers n                ; nombre de slots du sélecteur
@savepoint                    ; sauvegarde silencieuse (slot 1)
@saveon / @saveoff            ; autorise/interdit la sauvegarde

Contrôle de script / système

@for var = init to end [step n] ; boucle (corps jusqu'à @next, imbricable)
@next / @break                  ; itération suivante / sortie de boucle
@notif cond                     ; inverse de @if (exécute si la condition est
                                ; fausse, sinon saute au @endif)
@jumpf / @jumpb                 ; saut au marqueur ~ suivant / précédent
~                               ; marqueur de croisement (ligne seule)
@tablegoto var *l0 *l1 …        ; saut par table (index var → étiquette)
@skip n                         ; saute les n lignes suivantes
@resettimer                     ; remet le chronomètre interne à zéro
@gettimer var                   ; var = millisecondes depuis le dernier reset
@waittimer ms                   ; attend que le chronomètre atteigne ms
@date y m d / @time h mi s      ; date/heure UTC courantes
@getwindowsize w h              ; résolution logique dans w/h
@caption "titre"                ; titre de la fenêtre
@reset                          ; retour au *start (variables locales vidées)
@definereset                    ; rechargement du script + reset total
@setwindow x y col lig fw fh esx esy vite gras ombre #couleur x1 y1 x2 y2
                                ; boîte de texte : position, taille, police,
                                ; espacement, vitesse, couleur, zone
@setwindow3 …                   ; idem (ne touche pas au journal)
@setwindow4 …                   ; variante position/px (mêmes effets)
@transmode n                    ; accepté mais non appliqué (transparence fixe)

Sprites

@lsp nom fichier [x y]      ; charge (ou remplace) un sprite à une position
@msp nom dx dy              ; déplacement relatif
@amsp nom x y               ; déplacement absolu (équivalent de @move)
@csp nom                    ; retire un sprite
@allsphide / @allspresume   ; masque / affiche tous les sprites
@getspsize nom %w %h        ; largeur/hauteur de l'image dans deux variables
@getspmode %vis nom         ; 1 si le sprite est visible, sinon 0
@bgcopy                     ; capture l'écran courant comme nouveau fond
@print / @repaint / @blt / @ofscpy ; rendu immédiat (acceptés, sans effet)

Texte & couleur

@textcolor #rrggbb           ; couleur de base du dialogue (défaut : #f5f5eb)
@textclear                   ; efface le texte affiché (lignes NVL)
@puttext "texte…"            ; affiche un court texte (interpolation `%n`/`$n`)
@gettext var                 ; copie le texte courant dans une variable ($…)
@bar n valeur max larg haut x y #couleur ; barre de progression
@barclear                    ; efface toutes les barres
@prnum n valeur max x y pix d #couleur   ; compteur numérique
@prnumclear                  ; efface tous les compteurs
@locate x y / @shadedistance dx dy       ; acceptés, sans effet (disposition NVL)

Audio & vidéo (ONScripter)

@bgm / @mp3 / @play / @loopbgm fichier [loop|once] ; aliases musique
@bgmonce / @playonce fichier  ; musique en lecture unique
@bgmstop / @mp3stop / @playstop / @loopbgmstop ; coupe la musique
@wave / @dwave fichier        ; effet sonore (alias de @sfx)
@waveloop fichier             ; boucle une piste (musique)
@wavestop                     ; coupe les effets sonores
@stop                         ; coupe musique + effets
@bgmfadeout ms                ; fondu de volume musique vers 0 puis arrêt
@bgmfadein [ms|fichier]       ; fondu entrant (500 ms par défaut)
@getbgmvol var / @getmp3vol var ; volume musique en % (0-100)
@getsevol var / @getvoicevol var ; volume effets en %
@bgmdownmode n / @chvol ch n  ; acceptés, sans effet (pas de canaux dédiés)

Le journal (historique des pages de texte) s'affiche à l'écran avec le « Journal » du menu : ↑/↓ pour naviguer entre les pages, Échap pour fermer.

Variables globales préfixées $ (persistées dans save/global.sav), locales sans préfixe. Les variables indexées %n / $n suivent la même règle de portée (% → locale, $ → globale). Une condition (@if, @elseif, @notif) est une comparaison unique variable op valeur (== != < <= > >=) ou un nom de variable « vrai » ; les opérateurs logiques &&, ||, ! ne sont pas gérés (utiliser des @if successifs).

Sauvegardes

  • save/global.sav : JSON des variables globales (réécrit à chaque @set).
  • save/slotN.sav : JSON avec la position dans le script, les lignes déjà lues (pour « skip read »), les variables locales et l'horodatage.
  • Le slot 0 est la sauvegarde rapide (F5/F9).

Agencement des ressources

Les jeux suivent l'agencement de dossiers type ONScripter (template Onscripter-en_Game-Template). Les ressources sont cherchées dans trois racines, par ordre de priorité : patch/ (mods), puis la racine du moteur (le dossier courant), puis game/ (ressources du jeu). Dans chaque racine, les dossiers sont scannés par type de ressource :

Commande Dossiers cherchés (ordre)
@bg bgimage/, image/
@sprite fgimage/, image/
@music bgm/, sound/
@sfx sound/, voice/
@video video/, others/
@font system/, fonts/
--script system/first.thn par défaut ; chemin exact (.thn puis .tho), sinon scenario/

Un nom contenant déjà un séparateur (image/logo.png) est utilisé tel quel ; sinon vnth essaie chaque dossier du type dans l'ordre puis retombe sur la racine, pour rester compatible avec les jeux plats. Un fichier placé dans patch/ masque donc la version du jeu. Voir DOC/ressources.md pour le détail (y compris les archives .arc).

Démo

game/system/first.thn est le bootstrap (config du moteur puis @call scenario/first.thn), et game/scenario/first.thn l'entrée du scénario (courte ouverture puis @call intro.thn). game/scenario/intro.thn est la démo principale (fonds, sprites, dialogue interpolé avec @/\, couleurs #rrggbb, variables indexées, conditionnel, @rnd/@inc, choix, sauvegarde, menu clic-droit @rmenu, boucle @for, @date/@time), avec ses ressources réparties (bgimage/, fgimage/, bgm/, sound/, others/). Lancez-le :

cargo run

(le --script par défaut system/first.thn est cherché dans les racines, ici trouvé sous game/system/first.thn ; il enchaîne sur game/scenario/first.thn puis game/scenario/intro.thn). game/scenario/videotest.thn teste @video ; lancez-le avec --label videotest. @font accepte aussi un chemin absolu.

Propagation chiffrée (.tho)

Pour distribuer un scénario sans révéler son contenu : chiffrez-le avec l'outil tools/rozier (chiffre de Rozier, clé unique = texte de .forgejo/arckey.txt, alphabet UTF-8) :

cargo build --release --manifest-path tools/rozier/Cargo.toml
target/release/rozier encrypt game/scenario/intro.thn   # -> game/scenario/intro.tho

Le moteur (lancement avec --script ou défaut) déchiffre le .tho et exécute le scénario ; un .thn homonyme présent reste prioritaire.

Archives de ressources (.arc)

Un dossier de ressources peut être empaqueté en une archive chiffrée (.arc, chiffre de Rozier, même clé que les .tho). Un .arc X.arc fournit le dossier virtuel X/ : par exemple game/bgimage.arc remplace game/bgimage/. Le moteur utilise d'abord un fichier en clair s'il existe, sinon l'entrée correspondante de l'archive (extraite dans un dossier temporaire) — cela vaut pour les images, la musique et les effets, les polices et les vidéos, sans changer la syntaxe des scripts.

cargo build --release --manifest-path tools/arc/Cargo.toml
target/release/arc pack game/bgimage            # -> game/bgimage.arc
target/release/arc unpack game/bgimage.arc      # -> game/bgimage/
target/release/arc list game/bgimage.arc        # liste les entrées

Les scripts .thn gardent leur propre mécanisme (.tho).

Licence

MIT.