niepy — nie vu depuis Python
La porte d'entrée Python vers le moteur et les données de nie. C'est ce qui permet à un visual novel Ren'Py d'utiliser le vrai moteur et les vrais assets, sans réimplémenter quoi que ce soit.
Trois usages, du plus simple au plus engageant :
| Usage | Objet | Ce que ça fait |
|---|---|---|
| Lire le jeu | Vfs, decoder, g4tx_vers_png |
Le VFS (packs CPK ou dump) et les décodeurs de formats |
| Faire tourner le jeu | Match |
La simulation 11 v 11 déterministe, tick par tick |
| Rendre les assets | Rendu |
nie-game en sous-processus : textures et écrans composés → PNG |
| Lire les scènes | Scenario, Scene |
Les 5 173 scènes du jeu, leurs dialogues et leurs voix |
| Le mode histoire | StoryMode |
1 735 cinématiques, 889 cartes, 115 déclencheurs |
| Alimenter un VN | renpy.Catalogue |
Le catalogue d'assets exporté, et la génération de .rpy |
Prérequis
Tout passe par nie_ffi, la bibliothèque native construite depuis les crates Rust. Il faut
l'avoir construite au moins une fois :
cargo build -p aphrody-nie-ffi --release
niepy la résout seule : NIE_FFI_PATH si elle est posée, sinon target/release, sinon
target/debug, en remontant les ancêtres jusqu'à la racine du dépôt. Aucun chemin de
machine n'est écrit en dur.
Sur Windows, rustc produit
iecode.dll, sans préfixelib. Chercherlibiecode.dlléchoue silencieusement puis casse au premier appel, avec une erreur qui accuse l'appel et non la résolution du chemin.
Faire tourner un match
from niepy import Match
with Match() as m:
m.avancer(90.0) # 90 secondes de temps de jeu, à 60 Hz
print(m.score) # (domicile, extérieur)
print(m.porteur) # indice du joueur qui a le ballon, ou None
La simulation est déterministe : à dt et entrées identiques, deux exécutions donnent la
même suite d'états. C'est ce qui permet à un visual novel de rejouer une action à l'identique,
ou de calculer une issue de match hors écran puis de la raconter.
m.instantane() rend l'état complet en dictionnaire, en une seule traversée de frontière —
préférable à une boucle sur m.joueur(i) quand on veut tout.
Lire les données du jeu
from niepy import Vfs
with Vfs() as vfs:
print(len(vfs), "entrées indexées")
donnees = vfs.charger("data/common/…") # lit ET décode selon le format détecté
La racine du jeu se résout à l'exécution : argument explicite, puis NIE_GAME_DIR, puis
remontée des ancêtres à la recherche de data/cpk_list.cfg.bin.
decoder() sert tous les formats du dispatch partagé, sans une ligne de code par format
dans niepy — y compris les trois du mode histoire, qui ne rendaient qu'un en-tête jusqu'à
récemment :
| Format | Ce que decoder() rend |
|---|---|
.g4cm |
Caméra de cinématique : clips, names, objects, channels, times |
.g4nv |
Navmesh : vertices, polygons, edges, corners |
.lua.bin |
Bytecode Lua 5.2 : en-tête et prototypes imbriqués (main.code, constants, protos) |
Borner la mémoire
Le VFS garde les octets bruts de chaque paquet CPK ouvert, pour éviter de le relire. Quelques lectures dans des paquets différents suffisent donc à retenir plusieurs centaines de mégaoctets — et le budget par défaut de la bibliothèque native est de 16 Gio, dimensionné pour un traitement par lots qui a la machine pour lui.
Un jeu n'est pas dans ce cas. Posez votre plafond au démarrage :
with Vfs() as vfs:
vfs.regler_budget_cache(256 * 1024 * 1024) # 256 Mio
...
print(vfs.stats_cache()) # {'octets': …, 'entrees': …, 'budget': …}
vfs.vider_cache() # rend la RAM immédiatement
Vider est sans danger pour une lecture en cours : elle détient sa donnée jusqu'au bout. Les lectures suivantes relisent depuis le disque — c'est le prix, assumé, de rendre la mémoire.
Vfsprend la racine, et passe<racine>/dataà la couche native. Lui donner directement le dossierdatadonne « impossible d'ouvrir cpk_list.cfg.bin », une erreur qui accuse le fichier.
Le montage « dump » n'indexe rien tant qu'on ne l'énumère pas : lire() résout par chemin,
mais chercher() et parcourir() construisent l'index — des minutes sur 255 000 entrées.
Rendre les vrais assets
from niepy import Rendu
r = Rendu()
r.capturer("data/common/…/menu.g4tx", "game/nie/images/menu.png")
r.capturer_region("atlas.g4tx", "icone_01", "game/nie/images/icone_01.png")
nie-game est atteint en sous-processus, jamais en process, et c'est délibéré : il n'a
pas de lib.rs, c'est un hôte wgpu. Charger un contexte GPU dans une bibliothèque elle-même
chargée par le Python de Ren'Py est une bonne façon d'obtenir des plantages illisibles. Le
dépôt applique déjà cette règle à la CLI Rust nie, pour la même raison.
Seuls les modes hors-écran sont exposés. --window et --play ouvrent une fenêtre et ne
rendent la main qu'à sa fermeture : les appeler depuis un jeu déjà lancé le bloquerait.
Le binaire se construit à part :
cargo build -p aphrody-nie-game --release
Lire les scènes — ce dont un VN a réellement besoin
Le jeu porte 5 173 scènes réparties sur 45 chapitres, identifiées par une clé
evNN_NNNNN. Cette clé appaire des ressources dispersées dans quatre endroits du VFS :
| Ressource | Emplacement | Volume |
|---|---|---|
| Texte des dialogues | data/common/text/<langue>/event/ |
44 241 fichiers |
| Scripts de scène | data/common/event/ |
56 450 |
| Lipsync | data/common/sound/<langue>/*.p3lip |
21 047 |
| Sous-titres | gamedata/event/subtitle/<langue>/ |
~1 400 |
from niepy import Scenario, Vfs
with Vfs() as vfs:
scenario = Scenario.indexer(vfs) # parcourt le VFS une fois
scenario.sauver("game/nie/scenes.json") # …et ne le refait plus
scene = scenario.scene("ev01_01700")
lignes = scenario.lignes(scene, "fr", vfs)
Le texte existe en neuf langues, le doublage en deux. Mesuré sur l'installation de référence : 3 973 scènes traduites en français, mais zéro doublée ; le japonais en double 2 989, l'anglais 538. Un VN qui suppose que toute langue jouable est doublée se trompera sur sept langues sur neuf — d'où
Scene.est_doubleeetLANGUES_DOUBLEES.
En ligne de commande :
uv run python -m niepy scenes --index game/nie/scenes.json # index + chiffres
uv run python -m niepy scenes --out game/nie/scenes --langue fr # export des répliques
uv run python -m niepy scenes --out … --chapitre ev01 --limite 50 # un chapitre seulement
Le mode histoire — cinématiques, cartes, progression
uv run python -m niepy story --index game/nie/story.json # index + chiffres
uv run python -m niepy story --cle ev60_01560 # détail d'une cinématique
uv run python -m niepy story --acteurs 10 # les acteurs les plus présents
uv run python -m niepy story --out game/nie/story # fiches JSON
Mesuré sur l'installation de référence : 1 735 cinématiques (7 808 plans, 132 acteurs, 1 214 avec caméra), 889 cartes (dont 153 avec navmesh) sur neuf zones, 9 tables globales et 115 déclencheurs, tous porteurs d'une table décodable.
Le liant d'une cinématique n'est ni l'acteur ni la prise, mais le plan — le suffixe
_cNNNN, seul élément quasi universel du nommage. La forme <clé>_<acteur>_sNN_pNN_cNNNN
ne couvre que 52,4 % des noms ; s'y limiter perdrait la moitié du corpus, dont toutes
les caméras, qui ne portent ni acteur ni numéro de plan.
Deux limites à connaître. Les
.g4cm(caméras) et.g4nv(navmesh) sont indexés et localisés, mais leur décodage ne rend pour l'instant que l'en-tête — pas les trajectoires ni les polygones. Les.lua.bindes déclencheurs sont du Lua compilé, non décodé ; leur table.cfg.binjumelle, elle, se lit (Trigger.exploitable).
Alimenter un projet Ren'Py
D'abord produire les assets, depuis la CLI Rust :
# Les assets : voix, portraits, musique + catalogue.json. En Rust, car il faut lire les CPK.
nie vn export --out <projet-renpy>/game/nie
# Le .rpy qui les déclare, et les données du jeu en JSON. En Python, car c'est un artefact
# du monde Ren'Py.
uv run python -m niepy renpy --out <projet-renpy>/game/nie
uv run python -m niepy data --out <projet-renpy>/game/nie/data
Les treize familles de data visent des dossiers du VFS, jamais des fichiers : les noms
du jeu portent un numéro de version (chara_base_1.03.98.00.cfg.bin) qui change à chaque
patch. Leur poids réel surprend — menu compte 3 866 fichiers et event pèse 14,7 Mo, quand
item et team en comptent 5 et 6. --familles evenements,personnages restreint l'export.
Puis, dans un init python: du projet :
from niepy.renpy import Catalogue
catalogue = Catalogue.charger(config.gamedir + "/nie")
perso = catalogue.par_code("c01000010")
Le .rpy généré est régénérable : il ne contient aucune écriture à la main et peut être
réécrit à chaque export sans rien perdre.
Tests
uv run --with pytest pytest -q tests/
Les tests du moteur ne touchent pas au VFS : ils tournent sans les données du jeu. Ceux du pont Ren'Py travaillent sur un catalogue synthétique, hors moteur Ren'Py — c'est le seul moyen d'attraper une erreur de chemin d'asset avant de lancer le jeu.
Pourquoi ctypes et aucune dépendance
Ren'Py embarque son propre Python, où installer des roues tierces est une source d'ennuis sans
fin. niepy n'utilise donc que la bibliothèque standard : tout le travail réel est fait par la
couche Rust.
RE anchors
Knowledge base (var/nie.sqlite) tables:
function— 117 068 functions ofnie.execoverage— binary coverage rateshash_name— VFS and UI CRC32 name entriespdata_func—.pdatafunction entry boundaries
Key binary addresses:
0x1404ecd60— Core character controller0x1406d5840— Core game tick loop
Metadata
Release files for niepy 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| niepy-0.2.1.tar.gz | 83.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| niepy-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 153.7 kB
Release files / niepy-0.2.1.tar.gz
| Download URL | niepy-0.2.1.tar.gz |
|---|---|
| Size | 83.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c6c960bd1f7e771edbc351e2fb750590257cfc52ce92025b14a11ebe834a6ab8
|
|
BLAKE2b-256 checksum How to use checksums |
696df17669c6b3d3432bcd095b84d9e5f3beacf03bcf29a18ae2b5a66a62381b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / niepy-0.2.1-py3-none-any.whl
| Download URL | niepy-0.2.1-py3-none-any.whl |
|---|---|
| Size | 69.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
57207a1c5cae51b3263cfb387613ebb312af9f90af2a7ff984bdc78a6befbaa4
|
|
BLAKE2b-256 checksum How to use checksums |
96052885f29642a484bc489225b1b04979497eded0b8629e9a42dd4d39749087
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|