- Rust 99.4%
- Shell 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .cargo | ||
| .forgejo | ||
| DOC | ||
| game | ||
| src | ||
| tools | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| INSTALL.md | ||
| prepare-rt.sh | ||
| README.md | ||
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), redimensionnementcover. - Audio : musique (
@music file loop|once), effets (@sfx file), volumes. - Vidéo :
@video file(viaffmpeg/ffprobe, décodé en BGRA). - Effets :
@fadede transition,@window WxHpour 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)
-
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 desdl3-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" -
En-tête XScrnSaver manquant. La compilation from source de SDL3 échoue si
X11/XScrnSaver.hn'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) -
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.shles copie vers<target>/rt/lib(le binaire les retrouve via son rpath). AucunLD_LIBRARY_PATHn'est nécessaire. -
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],%net$n(variables indexées) sont interpolés ;#rrggbbcolore la suite du texte ;\nforce un retour à la ligne ;@attend un clic (même page),\attend un clic puis efface la page (_@/_\au début annulent l'attente). #labelou*label: étiquette (point de saut).> texte -> label: option de menu, affichée par la commande@choice(les lignes>qui suivent@choiceforment 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.thnhomonyme 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.