Ren'Py Analytics
Structure folding, save-file safety checks, and (coming) story-flow analysis and writing metrics for Ren'Py projects in VS Code.
Designed as a companion to vscode-language-renpy (highlighting, completion, navigation) and renpy-magic (LSP: rename, references, lint). This extension deliberately stays out of their lanes — its focus is story structure, reachability, and writing statistics. See SPEC.md for the architecture and ROADMAP.md for what's next.

Features (0.6 — M1–M6)
Structural folding
Real folding ranges for .rpy/.rpym files — not indentation guessing:
label bodies (which end at the next top-level statement, something indentation folding gets wrong)
menu blocks and each individual choice
screen, init, init python, and python blocks
Commands:
- Ren'Py: Fold All Labels — collapse the active file to its label skeleton.
- Ren'Py: Fold All Menus — collapse every menu block and choice (prose stays visible).
- Ren'Py: Fold All Labels and Menus — fully collapse everything foldable; menus stay collapsed even after you unfold a label. Unfold with VS Code's built-in Unfold All (
Ctrl+K Ctrl+J).
Save-file safety diagnostics
Ren'Py only stores variables changed after init in save files, and rollback only tracks what it knows about. This extension statically detects the classic footguns and reports them in the Problems panel:
| Rule |
Example |
Why it's a risk |
define-mutable-literal |
define inventory = [] |
In-place changes to a define'd list/dict/set are not saved and not rolled back |
define-reassigned |
define max_hp = 100 … $ max_hp = 200 |
define is for constants; runtime changes break rollback/save-load assumptions |
define-mutated |
$ inventory.append("sword") |
The mutation silently disappears on save/load |
missing-default |
$ mood = "happy" with no default mood = ... |
Older saves loaded into a newer build hit NameError when the variable is read before that line runs |
init-variable-changed |
init python: chapter = 1 … $ chapter = 2 |
Init assignments re-run on every start and fight with values restored from saves |
Suppress a finding with a comment on the same or preceding line:
define palette = [] # renpy-analytics: save-safe
Disable entirely with the renpy-analytics.saveSafety.enabled setting. Run on demand across the workspace with Ren'Py: Analyze Save-File Safety (Workspace).
Project analysis
Ren'Py: Analyze Project builds a label flow graph across the whole workspace (jumps, calls, menu-choice bodies, fall-through, renpy.jump("…"), and screen actions like Jump("x")) and reports:
- Inaccessible labels — labels unreachable from any entry point (
start, splashscreen, after_load, screen actions, …). Reported in the Problems panel and the Ren'Py Analytics activity-bar view. Dynamic jumps (jump expression …) are counted and noted, since they make reachability undecidable — set renpy-analytics.dynamicJumpMode to lenient to downgrade findings when they exist, or tag intentionally-dynamic targets with # renpy-analytics: reachable.
- Per-file statistics — labels, menus, choices, and words per file.
- Per-character statistics — words, dialogue lines, and average sentence length per character (Ren'Py text tags stripped;
extend lines attributed to the previous speaker; narrator tracked separately). Ad-hoc string speakers ("Name" "dialogue") are distinct characters in Ren'Py and are labeled "Name" (string speaker) so they can't be mistaken for a defined Character.
- Speaker consistency — a
duplicate-speaker finding (Problems panel + tree view) whenever a string speaker matches a defined character's display name or variable, e.g. "Lady Eleanor" "..." in a script that defines lady_eleanor = Character("Lady Eleanor"). Usually an authoring slip — the string form ignores the Character's styling and splits word counts. Suppress intentional distinct voices with # renpy-analytics: string-speaker on or above the line.
Ren'Py: Export Analysis Report writes the full analysis as Markdown (for humans) or JSON (for CI).

Current File pane
Below the project tree, the Current File pane follows the active editor and shows writing/narrative stats for just that script:
- Overview — word count and reading time (
renpy-analytics.readingSpeedWpm, default 200), labels/menus/choices, dialogue lines with narration share, average sentence length, and pacing (words per choice; "kinetic" when the file has no choices).
- Inaccessible labels and Issues (save-safety + speaker findings) scoped to this file, click-to-jump.
- Characters — per-character words with share-of-file %, line counts, and average sentence length.
- Scenes — dialogue words per label, largest first, for spotting unbalanced scenes.
- Connections — which labels in other files jump/call into this file, and where this file jumps out to.
Entry points can be extended with the renpy-analytics.extraEntryPoints setting.

Flag & variable explorer
The Variables pane (activity bar) replaces the flag-tracking spreadsheet: every story variable with its declaration, every write — including which menu choice triggers it — and every read, with condition reads marked as gates. Click any site to jump to it. Two orphan groups surface likely bugs at the top:
- Gated but never set — conditions read the flag, but nothing ever assigns it: the gated branch can never trigger.
- Never read — flags written and then forgotten (defines are exempt:
scene/show usages aren't expression reads).
Engine namespaces (config.*, gui.*, style.*, …) and Character(...)/image defines are excluded; attribute writes like player.hp -= 5 are credited to the declared player object; persistent.* variables are tracked automatically.

Choice-consequence CodeLens
Above every menu choice, a summary of what picking it does — stat changes, function calls, and where the story goes:
+corruption story.set_prologue_why_write=… ƒ apply_balanced_effect → day100_2_evening_flashback
"To catalogue what power hides. [[Curious, corrupting]]":

Consequence extraction adapted from universal-renpy-walkthrough (MIT), computed statically at edit time. Toggle with renpy-analytics.codeLens.enabled.
Story flow graph
Ren'Py: Show Story Flow Graph opens an interactive map of the whole story: label nodes plus menu-choice nodes, jump/call/fall-through edges, entry points outlined green, inaccessible labels red, dynamic jumps as a dashed pseudo-node. Drag to pan, scroll to zoom, click any node to open it in the editor.
Ren'Py: Show Story Flow Graph (Current File) scopes the map to the active script (e.g. one day file): everything in the file plus a dimmed, dashed ring of external neighbors — the labels in other files that jump in, and the ones this file exits to — all still clickable.
Ren'Py: Export Flow Graph (DOT) writes either graph (it asks: whole project or current file) as Graphviz DOT for rendering with dot -Tsvg or any DOT viewer.
Edges leaving an if/elif/while block carry the condition as an edge label (webview and DOT), and hovering a choice node shows its consequence summary — which flags it sets and where it leads.
Playtest from here
Ren'Py: Playtest From Here (command palette or editor right-click) launches the game warped to the line under the cursor — no more replaying from start to reach the scene you just wrote. It saves the file, finds the project root (the folder above game/), and runs the SDK with --warp game/…/file.rpy:line.
The SDK is located via the renpy-analytics.sdkPath setting; when unset, common install locations are scanned for renpy-*-sdk folders (newest first) and your pick is remembered. Warp requires config.developer to be true, which Ren'Py enables by default for un-built projects. To seed variables for warped scenes, define an after_warp label in your game.
Dead-end detection
Reachable labels whose flow runs off the end of the file without return/jump — a runtime error waiting to happen — are flagged in the Problems panel and listed in the Project Analysis view.
Playtime estimate
The Project Analysis view shows an estimated total playtime (all reachable dialogue at renpy-analytics.readingSpeedWpm) — an upper bound, since branches are mutually exclusive.
Ren'Py: Generate File Footer (or … for All Files) appends a summary comment block to the end of a script:
# ==== renpy-analytics:begin (auto-generated - do not edit; regenerate via VS Code) ====
# File summary - generated 2026-07-06
# Labels: 12 | Menus: 4 (11 choices) | Total words: 3,482
#
# Words per character:
# e (Eileen): 1,204 words | avg sentence 9.3
# narrator: 1,388 words | avg sentence 12.0
#
# Classes/functions called:
# Character, bonus_points, renpy.pause
#
# Variables referenced:
# mood, route_flag, trust
# ==== renpy-analytics:end ====
Regeneration replaces the block in place — never duplicates it — and preserves the file's line endings. Configure which sections appear with renpy-analytics.footer.sections; enable renpy-analytics.footer.onSave to auto-refresh footers on save (only in files that already have one).
Scoping to one game folder
By default every .rpy in the workspace is analyzed. In a monorepo with several copies of a game (e.g. prod/ and nonprod/), run Ren'Py: Select Game Folder — it detects candidate game roots (folders containing options.rpy/script.rpy) and saves your pick to the workspace setting renpy-analytics.gameDir. The current scope is shown at the top of the Ren'Py Analytics view; click it to change.
Commands (Ctrl+Shift+P)
All commands are prefixed Ren'Py: in the command palette. First-time users can also follow the Get started with Ren'Py Analytics walkthrough (Help → Get Started), which runs each of the core commands with one click.
| Command |
What it does |
Ren'Py: Analyze Project |
Build the flow graph + metrics; populate views and Problems panel |
Ren'Py: Show Story Flow Graph |
Interactive story map (whole project) |
Ren'Py: Show Story Flow Graph (Current File) |
Story map scoped to the active script |
Ren'Py: Export Flow Graph (DOT) |
Graphviz DOT export of either graph |
Ren'Py: Playtest From Here |
Launch the game warped to the cursor line |
Ren'Py: Fold All Labels |
Collapse the file to its label skeleton |
Ren'Py: Fold All Menus |
Collapse menu blocks and choices |
Ren'Py: Fold All Labels and Menus |
Collapse everything foldable |
Ren'Py: Analyze Save-File Safety (Workspace) |
Save-safety scan on demand |
Ren'Py: Export Analysis Report |
Markdown (humans) or JSON (CI) report |
Ren'Py: Generate File Footer |
Append/update the summary footer in the active file |
Ren'Py: Generate Footers for All Files |
Same, workspace-wide |
Ren'Py: Select Game Folder |
Scope analysis to one game root (monorepos) |
Development
npm install
npm run compile # type-check + build to out/
npm test # unit tests (vitest)
Press F5 in VS Code to launch an Extension Development Host.
Credits
- Story-graph concepts and annotation-tag conventions inspired by renpy-graphviz (AGPLv3 — no code reused; independent TypeScript implementation).
- Choice-consequence analysis ideas (planned) adapted from universal-renpy-walkthrough (MIT).
License
MIT