Project files and the .ripple folder
A project is a folder. The app writes no scaffold into it and imposes no layout. Its own bookkeeping is confined to one directory, .ripple/, at the project root. Every file the app owns is plain text.
File kinds
Section titled “File kinds”Three kinds are JSON namespaced inside .json, so ordinary tools still read them. A state machine is a mermaid diagram, and a script is Python.
| Pattern | Holds |
|---|---|
*.graph.json |
A board: nodes, wires and the choices on them |
*.macro.graph.json |
A macro: a board that a board can call |
*.component.json |
A component: a name, a cover and an ordered list of typed attributes |
*.soundscape.json |
A soundscape: a timeline of clips a passage plays |
*.fsm.mmd |
A state machine, as a mermaid stateDiagram-v2 |
*.loc.narr.bdl/ |
One passage in several languages: a folder of ordinary .md files |
*.py |
A script that a Script node runs |
.ripple/ |
The app’s own bookkeeping |
Every file the app owns carries an integer version, except three. A script is a .py file and a state machine is a mermaid diagram whose own header is its version, because neither is in a format of ours. A Markdown passage carries none either; the touch.json in its folder does.
A path inside a file is measured from the project root, with forward slashes, and never contains .., except a link in a passage’s prose (see Passages). A path outside the project is written absolute. The per-folder state in .ripple/ is written with sorted, relative paths.
The .ripple folder
Section titled “The .ripple folder”Nothing the app keeps about a project is written into your story. It lives in .ripple/, beside your files.
Directorymy-story/
Directory.ripple/
- project.json the title, description, cover and languages
- registry.json the index of boards, components and the other files, and their ids
- workspace.json the layout, the open documents, the side bar and Messages, and which folders were open
- cvars.json the console variables the project declares
- callbacks.json the host callbacks the project declares
- dictionary.json the words added to the spelling dictionary
- lints.json the diagnostics the author has answered
Directorysaves/ paused and bookmarked playthroughs
- …
Directoryexport/ export profiles
- …
Directoryexternal/ pictures and audio picked from outside the project, copied in
- …
Directorystyle/ the project’s own style rules, one Weir file each
- …
Directorychapters/
- the_gates.graph.json
Directorythe_gates_opening.loc.narr.bdl/
- en-US.md
- it-IT.md
- touch.json
- tavern.soundscape.json
Directorymusic/
- tavern.ogg
Directoryitems/
- sword.component.json
Directoryscripts/
- damage.py
- notes.md
| Entry | Holds | Version control |
|---|---|---|
project.json |
The title, description, cover and languages | Travels |
registry.json |
The index of every file in the project, by catalog id; the boards a story may begin at; the links between boards | Travels |
workspace.json |
The layout, the open documents, the side bar and Messages, and which folders were open. Per machine | Ignored by the built-in version control |
cvars.json |
The console variables the project declares. See Console variable declarations | Travels |
callbacks.json |
The named host callbacks: a name, an event and what the event is aimed at. See Host callbacks | Travels |
dictionary.json |
The words added to the spelling dictionary, in sorted order. A newer version is refused and never written over. See How to check spelling and grammar | Travels |
lints.json |
What the author has answered about the boards, for one card and one rule, keyed by lint name. Written even when empty. Not catalogued. A version this build does not know, or a file that is not this shape, reads as nothing answered, so the warning returns | Travels |
saves/ |
Paused and bookmarked playthroughs, one file a slot | Ignored by the built-in version control: a playthrough is a fact about your reading, not about the story |
export/ |
Export profiles, as <name>.profile.json. See Export options and bundle contents |
Travels |
external/ |
Pictures and audio picked from outside the project, copied in | Travels |
style/ |
The project’s style rules, as <name>.weir, in Weir, Harper’s rule language. No version of the app’s; a rule that does not compile is reported, never refused. See How to write a style rule |
Travels |
The registry
Section titled “The registry”registry.json has a version, roots (the boards a story may begin at, by id; authored, never derived), links (where the boards Jump to, derived from the boards and cached) and entries (every file the last walk found: id, kind, name, path).
- A Jump stores the target’s catalog id; the ids live in the registry.
- An id is 32 lowercase hexadecimal characters, minted once when a file is first seen and kept for as long as the path is known.
- A file is indexed as soon as it is in the folder, whether or not the app made it, so that a picker can offer it. A file imported from outside the project is listed as it lands.
- Nothing under a dotted folder is indexed, nor is a dotted file, nor anything outside the project folder. The one exception is a file directly under
.ripple/external/. - What an export or a compiled campaign carries is decided by what points at a file, not by the index, so the index adds nothing to either.
kind |
File | Recognised by |
|---|---|---|
board |
*.graph.json |
Suffix |
macro |
*.macro.graph.json |
Suffix, tested before board |
component |
*.component.json |
Suffix |
script |
*.py |
Extension |
narrationBundle |
*.loc.narr.bdl/, a directory |
Suffix |
machine |
*.fsm.mmd |
Suffix |
soundscape |
*.soundscape.json |
Suffix |
audio |
An audio extension; see Viewer kinds | Extension |
asset |
Anything else: an image, a video, a PDF, a loose note | A file that is none of the above |
name is the file’s stem for every kind except asset and audio, which keep the whole basename (map.png). path is root-relative.
Files from outside the project
Section titled “Files from outside the project”A picture or a sound picked from outside the project is copied into .ripple/external/, not into a folder the app invents at the top of the story. Copying in is a switch, Import outside assets, a checked row of the File menu and a palette command; with it off the file is linked. A link that leaves the project is reported under the field and in Messages every time the project is opened.
Viewer kinds
Section titled “Viewer kinds”The viewers recognise a file from its name alone, lowercased. Compound suffixes are tested first, and a macro’s before a board’s.
| Kind | Matches |
|---|---|
macro |
.macro.graph.json |
graph |
.graph.json |
component |
.component.json |
soundscape |
.soundscape.json |
narrationBundle |
.loc.narr.bdl |
machine |
.fsm.mmd |
script |
.py |
pdf |
.pdf |
svg |
.svg |
raster |
.png .jpg .jpeg .gif .webp .bmp |
audio |
.mp3 .wav .ogg .oga .flac .m4a .aac .opus |
markdown |
.md .markdown |
text |
Everything else |
A text file above 4 MiB is refused by the text viewer, and a <locale>.md above 4 MiB is skipped by the passage reader.
Components
Section titled “Components”{ "version": 3, "name": "Goblin", "cover": "art/goblin.png", "attributes": [ { "label": "HP", "type": "int", "value": 7 }, { "label": "Alive", "type": "bool", "value": true }, { "label": "Bio", "type": "markdown", "value": "A small, angry thing." }, { "label": "Loot", "type": "list<asset>", "items": ["art/gold.png"] } ]}| Key | Rule |
|---|---|
version |
Format 3. An older file is lifted when it is opened or moved; a build that predates a format refuses a newer component rather than misreading it |
name |
String, default "" |
cover |
Optional string; root-relative, absolute when outside the project |
attributes |
Ordered. Each has label (default ""), type (default string), and value. list<asset> and list<componentRef> carry their paths under items instead |
Declared types
Section titled “Declared types”A declared type is a base at an arity, written base, list<base> or map<base>. A map is always keyed by strings. Eight bases at three arities make the twenty-four types that every editor in the app offers. The same set describes component attributes, node shapes, macro takes and console variables.
| Base | Default | Meaning |
|---|---|---|
int |
0 |
Whole number |
float |
0.0 |
Number |
bool |
false |
True or false |
string |
"" |
Text |
markdown |
"" |
Text, edited as Markdown |
asset |
"" |
A path to a file |
componentRef |
"" |
A path to another component |
json |
{} |
Any JSON value, stored decoded |
- Numbers and booleans are JSON numbers and booleans, never strings.
- An attribute whose type this build cannot name keeps whatever it arrived with.
numberandentityare read and never offered.numberis the older spelling of a number;entityis a name and reads asstring.- Older spellings read forever and are written in the new form the next time you touch the type menu:
assetListaslist<asset>,componentListaslist<componentRef>,numberListaslist<number>,boolListaslist<bool>,stringListaslist<string>. - Whether an attribute carries
itemsis decided by its type, never by its contents. - Coercion never throws. An
intfrom a float rounds, from a boolean is 0 or 1, and from a string parses, otherwise 0. Afloatlikewise, otherwise 0.0. Aboolfrom a number is non-zero and from a string is one oftrue,yes,1,on. Astringfrom a map or a list is its JSON. Ajsonfrom nothing is{}. Alist<T>ormap<T>coerces entry by entry. - An integer is at most 2⁵³ − 1.
- A component written by a later version keeps its paths, in lists and maps both, when you open and save it.
Paths in a component, the cover and each list<asset> item, are measured from the project root.
An asset or componentRef slot stores the path. Its chip is marked when the file is outside the project or missing.
Passages
Section titled “Passages”A passage on disk is a localised passage: a folder, the only kind of file of ours that is a directory, which the app shows as one document.
Directorychapters/
- the_gates.graph.json
Directoryreaching_the_gates.loc.narr.bdl/
- en-US.md the passage, in the language the story is written in
- it-IT.md its translation
- touch.json when each was last touched, so a clone can tell which is older
| Rule | Value |
|---|---|
| Name | The folder’s basename with .loc.narr.bdl removed |
| Name when the app creates it | <board>-<passage title>, beside the board: gate.graph.json and gate-at-the-gate.loc.narr.bdl/, so passages sort next to the board they belong to |
| A language file | <locale>.md, where <locale> is a known locale id exactly. No normalisation |
| Anything else in the folder | README.md, en.md, a subfolder: reported as unrecognised and never read |
| Languages read | Every language found, whether or not the project has it switched on |
| Size ceiling | A <locale>.md over 4 MiB is skipped. One unreadable file does not fail the passage |
| Links in the prose | A link the app writes starts with / and is measured from the project root: . A link written by hand without the slash is relative to the bundle’s parent folder:  beside chapters/the_gates.graph.json means art/map.png |
touch.json |
{ "version": 1, "touched": { "<locale>": "<ISO 8601, UTC>" } }, keys sorted. A cache: a problem reading it reads as empty, and a walk rebuilds it |


See Passages, choices, languages and reading time for what a passage holds.
Version gate
Section titled “Version gate”A file from a newer build is refused where it is authored and read as empty where it is a cache. A document the author cannot get back, such as the index, the project settings, a component or a board, stops the app rather than being half-read and written back over. A cache that a walk of the folder rebuilds simply rebuilds. touch.json and lints.json are caches.