Ren'Py Consistent Variables

Tran_the_plug

Newbie
Game Developer
Jul 1, 2025
15
18
Hello everyone,
I am working on a trample fetish roguelike game in RenPy and want to store the wins for each character and for each difficulty in a consistent dictionary. However, even though it works fine when I'm playtesting, it causes a game crash when other people try to play it.
Any help would be appreciated!

0pKe83n.png
 

Winterfire

Conversation Conqueror
Respected User
Game Developer
Sep 27, 2018
6,017
8,748
You need to initialize your variables.

-edit-
In case you want the error to appear in your playtesting as well, just delete your saves (local and appdata). That's where the variables causing issues are stored, and the issue is that they're not initialized. Always initialize them.
 

Tran_the_plug

Newbie
Game Developer
Jul 1, 2025
15
18
i use default to initialise them in a specific file just for that, still, it causes the error. Am I missing something?
Also, thank you for the saves, I didnt know they are stored in appdata as well.
 

Winterfire

Conversation Conqueror
Respected User
Game Developer
Sep 27, 2018
6,017
8,748
idk what "Limp" is, but that's where the error is. Since the error happens to players but not you, my guess is that you had initialized it at some point, but then changed it, so your saves are fine but new ones aren't.

To be clear, this is what I mean by initializing:
default Variable = ""

So rather than being null and returning an error, it'll be an empty string and show as such when called.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,246
19,524
You need to initialize your variables.
It's not exactly the issue he have. The issue isn't the variable itself, but the key for one of its entries.

His code looks like:
Python:
myDictionary = {}
[...]
if myDictionary["limp"] == True:
   [...]
The "myDictionary" dict exist, but there's no "limp" entry in it.


OP, I guess that the issue is because you wrote something like this:
Python:
label whatever:
    menu:
        "choice 1":
            $ persistent.sub_wins["Limp"] = True
            [whatever code]
        "choice 2":
            [whatever code]
You must create the entry in all case.
Either by giving it a negative-like value when needed; something like:
Python:
label whatever:
    menu:
        "choice 1":
            $ persistent.sub_wins["Limp"] = True
            [whatever code]
        "choice 2":
            $ persistent.sub_wins["Limp"] = False
            [whatever code]

Or by declaring all the possible entries right from the starts.
It's more complicated in this case, because you've to test every entries in order to not reset it. But if the entry can be checked before the event happen, you are limited in possibilities.
Something like:
Python:
init python:

    #  If the dictionary do not exist, create it.
    #  Like it's a persistent value, it can be safely done at /init/ level.
    if not persistent.sub_wins:
        persistent.sub_wins = {}

    #  Iterate through all the possible entries
    for atom in [ "Limp", all the other possible entries ]:
        #  And create it, with a default negative-like value if it don't already exist.
        if not atom in persistent.sub_wins:
            persistent.sub_wins[atom] = False
Alternatively you can change all the part of the code that try to access one of those entries. Something like:
Python:
if "Limp" in persistent.sub_wins:
    $ wins = persistent.sub_wins["Limp"]
else:
    $ wins = False
 

Tran_the_plug

Newbie
Game Developer
Jul 1, 2025
15
18
This is how the code looks like

Python:
        $ wins = persistent.sub_wins.get(name, [0, 0, 0])

        text "(Wins: Normal: [wins[0]] | Hard: [wins[1]] | Extreme: [wins[2]])" xalign 0.5 size 25
And the dictionary is defined like so

Python:
default persistent.sub_wins = {
            "Aiden": [0,0,0],
            "Ethan": [0,0,0],
            "Limp": [0,0,0],
            "Mason": [0,0,0],
            "Noah": [0,0,0]
            # Add other subs here
        }
Hope this helps!!!
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,246
19,524
This is how the code looks like
Python:
        $ wins = persistent.sub_wins.get(name, [0, 0, 0])
Sorry, but your screenshot of the error thrown include the actual code, and it's not this one.


Python:
default persistent.sub_wins = {
            "Aiden": [0,0,0],
            "Ethan": [0,0,0],
            "Limp": [0,0,0],
            "Mason": [0,0,0],
            "Noah": [0,0,0]
            # Add other subs here
        }
Well, strong guess: The error is implied by the comment line.

You update the code each time you add someone new. But, as the , "the default statement sets a single variable to a value if that variable is not defined when the game starts, or after a new game is loaded." [emphasis is mine]

Therefore, it works for you, because you restarted a new game, but it will not works for player loading a previous save, because the dictionary do not include the new entries.

You need to use a variation of the second to last code I gave above and the to update the dictionary:
Python:
label after_load:
    python:
        for atom in [ "Limp", all the other possible entries ]:
            if not atom in persistent.sub_wins:
                persistent.sub_wins[atom] = [0,0,0]
    return
 
  • Like
Reactions: osanaiko

Turning Tricks

Rendering Fantasies
Game Developer
Apr 9, 2022
1,843
3,424
Just a comment about the persistent variables the OP is trying to use - example persistent.sub_wins

Visual Code Studio throws an error when you use underscores in a persistent variable name. (also in MultiPersistent ones) I don't know why and, AFAIK, Ren'py is still okay with it. But if you use VCS (it's the default of the Ren'py SDK now, BTW) this can make it harder to debug your code, as well as it being annoying to see those problems on your open .rpy files.

I don't want to change those variables, this late in my VN's development; but on my next project I will be not using any underscores in variables like this. Just periods. I use way too many underscores as it is, anyhow. It's a poor habit I developed.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,246
19,524
Visual Code Studio throws an error when you use underscores in a persistent variable name. (also in MultiPersistent ones)
It's either a bug from VS or, more surely, from the definition file.

In Python underscore is not just a fully valid characters for variable and attribute names, the whole Python philosophy is to use snake_case for all names.

But yeah, it's annoying if VS throw an error for this.
 

Turning Tricks

Rendering Fantasies
Game Developer
Apr 9, 2022
1,843
3,424
It's either a bug from VS or, more surely, from the definition file.

In Python underscore is not just a fully valid characters for variable and attribute names, the whole Python philosophy is to use snake_case for all names.

But yeah, it's annoying if VS throw an error for this.
Ya, it bugs my slightly OCD self to constantly see the random error in my code, that isn't actually an error. I'm still learning VCS and haven't figured out how to ignore or stop listing this type of false error. As you can see from the screencap, VSC seems to cut off anything after an underscore. That's why it thinks I haven't defaulted that persistent variable, when I have.

VSC_error.JPG
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,246
19,524
That's why it thinks I haven't defaulted that persistent variable, when I have.
The "funny" part is that it don't complain that the line is wrong. What mean that it see the "preference.ntr_switch" when checking for syntax errors, but only "preference.ntr" when it come to checking if it have been declared or not.
 
  • Thinking Face
Reactions: Turning Tricks

osanaiko

Engaged Member
Modder
Jul 4, 2017
3,162
6,079
I checked the source for the Renpy Language extension for VSC

If there is a bug it's in

Code:
// File src/diagnostics.ts
// irrelevant parts snipped out

const rxPersistentDefines = /^\s*(default|define)\s+persistent\.([a-zA-Z]+[a-zA-Z0-9_]*)\s*=\s*(.*$)/g;
const rxPersistentCheck = /\s+persistent\.(\w+)[^a-zA-Z]/g;

function refreshDiagnostics(doc: TextDocument, diagnosticCollection: DiagnosticCollection): void {

    const diagnostics: Diagnostic[] = [];

    const persistents = [];
    if (dataLoaded) {
        // osanaiko: i think this might be a cache, or perhaps something to share the persistent definitions across the many files
        const gameObjects = NavigationData.data.location["persistent"];
        for (const key in gameObjects) {
            persistents.push(key);
        }
    }

    for (let lineIndex = 0; lineIndex < doc.lineCount; lineIndex++) {
        const line = NavigationData.filterStringLiterals(doc.lineAt(lineIndex).text);

        if (config.warnOnUndefinedPersistents) {
            checkUndefinedPersistent(diagnostics, persistents, line, lineIndex);
        }
    }
}


function checkUndefinedPersistent(diagnostics: Diagnostic[], persistents: string[], line: string, lineIndex: number) {
    let matches: RegExpExecArray | null;
    while ((matches = rxPersistentCheck.exec(line)) !== null) {
        if (line.match(rxPersistentDefines)) {
            if (!persistents.includes(matches[1])) {
                persistents.push(matches[1]);
                continue;
            }
        }
        if (!matches[1].startsWith("_") && !persistents.includes(matches[1])) {
            const offset = matches.index + matches[0].indexOf(matches[1]);
            const range = new Range(lineIndex, offset, lineIndex, offset + matches[1].length);
            const diagnostic = new Diagnostic(range, `"persistent.${matches[1]}": This persistent variable has not been defaulted or defined.`, DiagnosticSeverity.Warning);
            diagnostics.push(diagnostic);
        }
    }
}
If i understand correctly:

It loops all the code looking for Persistent references with regex: const rxPersistentCheck = /\s+persistent\.(\w+)[^a-zA-Z]/g;

Then if the line with "persistent" also matches the "define|default" regex, it adds the name to the "persistents" string array.

The define regex is this:const rxPersistentDefines = /^\s*(default|define)\s+persistent\.([a-zA-Z]+[a-zA-Z0-9_]*)\s*=\s*(.*$)/g;

Then in the same loop iteration, operating on the line matched by the outer regex, it set the error message is the persistent variable name is NOT in the list of persistents and does not start with underscore.

I don't see the bug.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,246
19,524
I don't see the bug.
Strictly speaking, I don't see the bug either.

But depending on the regEx implementation, I can see how the error can happen.
Side note: The both regEx are wrong, and the second is also dirty.


Declaration:
[...]persistent\.([a-zA-Z]+[a-zA-Z0-9_]*)\s*=[...]
A dot, [start capture] followed by at least one letter (uppercase or lowercase), possibly followed by as many letter (uppercase or lowercase) or a digit or an underscore, as there is [stop capture] until it will find an equal sign, with possibly one or more space before it.

Error: "persistent._variable" is a valid syntax, but wouldn't be caught.


Check:
[...]persistent\.(\w+)[^a-zA-Z]/g
A dot, [start capture] followed by at least a letter (uppercase or lowercase) or an underscore or a digit, [stop capture] followed by everything except a letter (uppercase or lowercase).

Error: will caught the invalid "persistent.3variable".



Now for the possible cause of the error:

Normally the \w+ should be greedy. If you have by example "bla_bla_345678 " it should catch everything, because it match the \w, until the "space" because it don't match the \w AND match the [^a-zA-Z].
But, as the error triggered show, it's not what happen. The first underscore will be seen as part of the [^a-zA-Z], not caring about the fact that it's also part of the \w, what will stop the capture.


The correct regEx should be:
const rxPersistentDefines = /^\s*(default|define)\s+persistent\.([a-zA-Z_]\w*)\s*=\s*(.*$)/g;
Will be captured anything that starts with a letter (uppercase or lowercase) or an underscore, possibly followed by one or more letter (uppercase or lowercase) or underscore or digit, until it will find an equal sign, with possibly one or more space before it.


const rxPersistentCheck = /\s+persistent\.([a-zA-Z_]\w*)\W/g;
Same as above, but the capture will stop when it will encounter the first character that isn't a letter (uppercase or lowercase) or an underscore or a digit.

Normally with those two regEx it should works. But since the previous one failed due to the regEx implementation, I can't guaranty it at 100%.
I built them taking count of the none greedy issue, but I don't guaranty that the implementation don't trigger others issues with this formulation.
 
  • Like
Reactions: osanaiko

osanaiko

Engaged Member
Modder
Jul 4, 2017
3,162
6,079
Nice catch! A classic case of "lets use regex for parsing... and now we have more problems"

The language for plugins in VSCode is Typescript, which is just javascript but wearing a hipster t-shirt.

Javascript regex "*" operator is supposed to be greedy by default. But we can't be sure about how the VSC runtime executes javascript, maybe it is not exactly to standard.

Given that there already seems to be a proper tokenizing/parsing into an AST, i don't know why they'd choose to go with something as fragile as this solution.

Good news for Turning Tricks however: you can turn off just the "undefined Persistent" check in the plugin settings -

1752409658341.png
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,246
19,524
A classic case of "lets use regex for parsing... and now we have more problems"
What came with the other classic "we need a limited version of the regEx we already have, so we will rewrite everything from scratch... What can goes wrong?"