Skip to main content

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éfixe lib. Chercher libiecode.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.

Vfs prend la racine, et passe <racine>/data à la couche native. Lui donner directement le dossier data donne « 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_doublee et LANGUES_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.bin des déclencheurs sont du Lua compilé, non décodé ; leur table .cfg.bin jumelle, 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 of nie.exe
  • coverage — binary coverage rates
  • hash_name — VFS and UI CRC32 name entries
  • pdata_func — .pdata function entry boundaries

Key binary addresses:

  • 0x1404ecd60 — Core character controller
  • 0x1406d5840 — 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)

Source distribution for niepy 0.2.1
File Size Uploaded
niepy-0.2.1.tar.gz 83.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for niepy 0.2.1
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page