Ren'Py What is the proper way of implementing a "uncensored" patch?

LewdWriter

Newbie
Oct 11, 2018
19
4
Hello, I ran into a thread yesterday that mentioned that having "forbidden" content in your scrip might get you into troubles even if you are hiding the content via a simple flag.

Some suggestions in that thread on how to do it properly where to use the config.labels_overrides and/or the config.say_menu_filters. However as I am fairly new I'm not sure exactly how I would use these.

I think I understand the label overrides, but not the say menu filters. Has a kind soul in this community created a guide to properly create these patches?

Thank you so much!
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,302
15,172
Has a kind soul in this community created a guide to properly create these patches?
Not really, but it's not this difficult to write them.
config.say_menu_text_filter is ; it mean that it's a function that will be called from another function, with the intend to either change the behavior of the calling function. In this particular case, the callback will be called each time a dialog line or a menu choice will be displayed. It take a string (the text that will be displayed as dialog line or menu choice) as argument and return a string (the text to effectively display).
Therefore, the more basic way to use it is like this :
You don't have permission to view the spoiler content. Log in or register now.

But as you can imagine, it's somehow a pain in the ass, because you need to have a big if structure to handle this. Hopefully, you can simplify this a little, letting Python do most of the works for you :
You don't have permission to view the spoiler content. Log in or register now.
Don't be afraid, the dictionary can be as big as you want without real impact. But if the game is linear, you can optimize it a little, with the help of config.labels_overrides :
You don't have permission to view the spoiler content. Log in or register now.
Note that for this to effectively works, the game need to be linear, because in the second_patched, you completely change the list of string to replace.

Finally, there's times where you don't need to replace a whole string but just a word ; by example "landlady" by "mom". Here again it's in the config.say_menu_text_filter that everything is done. But this time you'll need the help of Regular Expression ("RegEx" for short) [ | ].
You don't have permission to view the spoiler content. Log in or register now.
 

LewdWriter

Newbie
Oct 11, 2018
19
4
Thank you so much! This is a lot to take in but a great help. I think the final example is what I need since I’m looking to replace the relationship types for the most part.
 

Rich

Old Fart
Modder
Donor
Respected User
Game Developer
Jun 25, 2017
2,486
7,007
As usual, @anne O'nymous has given a nice, detailed description.

Just for discussion's sake, it's probably worth noting that, when implementing a "straight replacement" say_menu_text_filter as described by @anne O'nymous, you're essentially doing something similar to what Ren'py does itself when applying a - taking the original text and transforming it into some alternate text. The Ren'py translation support is more sophisticated, of course, in that you can replace a single line of dialog with multiple or vice versa. (Note that I'm not saying that the underlying mechanisms are the same - they're actually quite different - just that the concepts are at least vaguely similar.)

So, to echo some of the issues:
  • In the "look the entire line up in a map or an if statement and find the replacement line" approach, you potentially end up with a very large list of strings to replace. In addition, the input strings have to exactly match those in the dialog. Change a space or a capital letter, and the line won't match. This can make for some tedious work in creating and maintaining the replacement map. Doable, just tedious.
  • Taking a "use a regex to replace 'Landlady' with 'Mom'" approach, you can cut down on the amount of work you have to do, because you don't have to "map replace" every single line of text that uses "Landlady" and wants to be changed to "Mom." However, depending on the exact text you're working with, you can sometime run into problems with designing the replacements. Plurals and articles and things like that.
  • Neither of these approaches allow you to add additional lines of dialog, or remove them. To do that, you're going to have to resort to either putting labels around the block and using label replacement, or else use the language translation features. (Which can map N lines of input to M lines of output.)
As just an example of the second issue, consider the line
Code:
    e "Hey, did you talk to the landlady?"
    e "We both have hot landladies, don't we?"
In this case, you probably want the replacement to be
Code:
    e "Hey, did you talk to Mom?"
    e "We both have hot Moms, don't we?"
so you have to factor in the "the" and the plural in your replacement strategy. This is all doable, of course, it's just stuff you have to watch out for, particularly if you're using "word replacement" instead of "line replacement".

One other thing to point out is that the say_menu_text_filter is called before any text substitution is performed on the string. So if you have:
Code:
    $ var = 'Monday'
    e "Today is [var]"
the string your filter function will receive is "Today is [var]", not "Today is Monday". This is a good thing, because if you have something like:
Code:
    e "Hey, [player_name], did you talk to the landlady?"
you can still potentially do the "replace the entire string using a map" without having to worry about all the potential values that player_name could take on.


So, what about using Ren'py's translation support for this? Theoretically doable. The basic steps would be:
  1. Create a "translation" for a language. Maybe call the language "uncensored."
  2. Update the text where necessary
  3. Include the "uncensored language" files in your patch. (i.e. the files in game/tl/uncensored
  4. Have your patch include (probably as a late-stage init) a call to renpy.change_language()
This approach has some power, but also some pitfalls

Pros:
  • Ren'py generates files for you with all the items that have to be reviewed. So this partially automates the process of generating the "input" strings.
  • You can alter the number of lines spoken in places, if you need to.
Cons:
  • You still have the tedium of going through everything. In this case, what you'd probably do is delete the translation block for any text that doesn't have to be changed. If something isn't translated, Ren'py will fall back to the original text. But, i this case, if you miss something that needs to be "unaltered", that text will disappear from the game, because Ren'py defaults to generating empty strings. So this is something you have to be careful about.
  • More seriously, Ren'py's translation support is designed for translating fully-completed games, not for "as you go along." Because of the way that Ren'py generates the keys it uses for translations, if you go back and alter previously-translated text, or add labels or do any of the other myriad things you typically do during development, this will mess up the previously-generated translation, and you may have to start over again. (You can see the keys on the "translate" lines in the Ren'py sample here: . Ren'py generates them based on hashes of information in the blocks, so if you alter anything in the block, this changes the hash.)
There are some ways to get around the second issue, but they can be just as much of a pain. So while using an "uncensored translation" is a theoretically viable approach, it has its own set of issues. One of its few redeeming features is the "change the number of lines" feature, but you can get around that with the label replacement approach.

All of this assumes you're trying to 100% completely squeaky clean and have no trace whatsoever of your intent to publish uncensored text. There is an interim approach, in which you could set up replaceable text as variables, and have your patch change the values of the variables. So you use something like:
Code:
    e "Hey, [player_name], did you talk to [the_landlady_name]?"
and then have your base code set the_landlady_name to "the landlady" and your patch overwrite it to "Mom" later in the init sequence. Now, this does, to a small extent, expose the fact that you intended to muck about with text. But if you ever got called out on this (very unlikely) you could probably defend having coded that way by saying "Oh, we originally planned to allow people to change the names of the characters, and then abandoned that, but didn't go back and rewrite everything." Hard for someone to prove otherwise. Of course, if you take this approach, you want to make sure that your variable names are benign. Like, use "landlady_name" instead of "mom_name", since the latter kind of exposes your intent.

Anyway, since the thread title asked about approaches to patches, not just say_menu_text_filters, that's my $0.02 or $0.03 on the issue, over and above what @anne O'nymous gave you.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,302
15,172
The Ren'py translation support is more sophisticated, of course, in that you can replace a single line of dialog with multiple or vice versa. (Note that I'm not saying that the underlying mechanisms are the same - they're actually quite different - just that the concepts are at least vaguely similar.)
Yes, they are different ways to achieve the same result. As usual with Ren'py, you're rarely stuck with just one method. Which one to use depend of the amount of text that will need to be changed.


Change a space or a capital letter, and the line won't match. This can make for some tedious work in creating and maintaining the replacement map. Doable, just tedious.
In the fly, one workaround can be to have altered version for devel purpose.
Split the dictionary label by label, and delete each strings effectively displayed.
You don't have permission to view the spoiler content. Log in or register now.
Launch the label from the console, jump someLabel, then once you reached the end of the label, use the console to take a look at the dictionary validationChange.keys(). In this particular example, the second string have to '.', and will match nothing. So you'll see that this string is still in the dictionary and know what to look at.
Then, when everything is correct (the dictionary is empty at the end of the label), you can copy/paste the validated string into the dictionary used by the patch.


However, depending on the exact text you're working with, you can sometime run into problems with designing the replacements. Plurals and articles and things like that.
There's also the problem of string that are part of a word. Replacing "mom" by "landlady", by example, will make "moment" be displayed as "landladyent".
Here the workaround is to use the "\b" RegEx meta character. It mark the point between a letter and a "none letter". So, "\bmom\b" should match "mom" but not "moment" ; I use conditional because Python RegEx and I aren't really friends, I'm too corrupted by my beloved Perl RegEx.


One other thing to point out is that the say_menu_text_filter is called before any text substitution is performed on the string.
On a side note, it also let you do the opposite. In one of his mod, @bossapplesauce used this to replace a hardcoded name (defined by the author and not stored in a variable), by a variable. Which let the player name the sister, instead of having the name decided by the author.


So, what about using Ren'py's translation support for this?
For an example, use this method for its taboo patch.


There is an interim approach, in which you could set up replaceable text as variables, and have your patch change the values of the variables. So you use something like:
Code:
    e "Hey, [player_name], did you talk to [the_landlady_name]?"
More on the subject, and the code for a global patch, can be found .


that's my $0.02 or $0.03 on the issue
As useful and complementary as usual. I just added 0.01€ to this ; with the change it should double the sum :D
 
  • Like
Reactions: Rich

Rich

Old Fart
Modder
Donor
Respected User
Game Developer
Jun 25, 2017
2,486
7,007
Yes, they are different ways to achieve the same result. As usual with Ren'py, you're rarely stuck with just one method. Which one to use depend of the amount of text that will need to be changed.
Absolutely. In fact, I would hazard a guess that most "taboo patches" would probably use a combination of the techniques.


In the fly, one workaround can be to have altered version for devel purpose.
Split the dictionary label by label, and delete each strings effectively displayed.
Elegant solution.

There's also the problem of string that are part of a word. Replacing "mom" by "landlady", by example, will make "moment" be displayed as "landladyent".
Here the workaround is to use the "\b" RegEx meta character. It mark the point between a letter and a "none letter". So, "\bmom\b" should match "mom" but not "moment" ;
Yes, that's an excellent point. The use of the \b is VERY elegant - fixes the "moment" problem without requiring special casing for punctuation marks, beginning and end of strings, etc. This brings to mind another, related technique. If you were using regex substitution, you could put distinct tokens into your original text, and perform replacements in both the original and the patched game. For example, you could code:

Code:
    e "Have you talked to XYZZY?"
and then have the "standard" game use say_menu_text_filter to replace XYZZY with "the landlady" and the patched game replace it with "Mom". With a different token for each potential substitution, you avoid ambiguity. This would be effectively the same as using variables, just without the variables. I'm not sure that this conveys any advantages over the "variable" approach, other than perhaps confusing anyone looking at the code, since they'd see different text than showed on the screen, and unless they knew to look for a say_menu_text_filter (which isn't an extraordinarily well-known method) they'd probably be scratching their head. Particularly if you hid the filter function as a .py file (instead of a .rpy file) somewhere obscure.

I use conditional because Python RegEx and I aren't really friends, I'm too corrupted by my beloved Perl RegEx.
I don't use regex a lot, but I do constantly get tripped up by the differences between different language implementations, so join the club. (Or else I join your club...)

On a side note, it also let you do the opposite. In one of his mod, @bossapplesauce used this to replace a hardcoded name (defined by the author and not stored in a variable), by a variable. Which let the player name the sister, instead of having the name decided by the author.
Yes, good point. Basically, the say_menu_text_filter gets inserted fairly early in the process. What I haven't experimented with is where it falls in the chain when translations are also involved. Something I should check out, just to stock my bag of tricks. (If I had to guess, it's probably after the translation code has done its bit.)

As useful and complementary as usual. I just added 0.01€ to this ; with the change it should double the sum :D
OK, now we just need somebody to ante a peso and a couple of pence, and we can kick off the poker game... LOL
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,302
15,172
[...] then have the "standard" game use say_menu_text_filter to replace XYZZY with "the landlady" and the patched game replace it with "Mom". With a different token for each potential substitution, you avoid ambiguity. [...] I'm not sure that this conveys any advantages over the "variable" approach, other than perhaps confusing anyone looking at the code, [...]
The only difference is that it's a little slower. But here we are talking in micro seconds, if not nano seconds, so an imperceptible difference. For the rest, even the obfuscation can be done with the text substitution approach. Either something like my text patcher or a more basic object :
relationship.py
Python:
import renpy.store as store

store.myVar.add( "landlady", "mother" )
store.myVar.add( "roommate", "sister" )
script.rpy
Python:
init -1 python:

    class DynVar( renpy.python.RevertableObject ):
        def add( self, name, value ):
            if not hasattr( self, name ):
                setattr( self, name, value )

    myVar = DynVar()

init python:
    import relationship

label start:
    "Here you see my [myVar.landlady], and here there's my [myVar.roommate]."
    "FINISHED"
    return
You can even put directly the class declaration in the .py file if you want.


(If I had to guess, it's probably after the translation code has done its bit.)
It's after for a simple reason, Ren'py trick everybody with the translation. As far as I understood it, the say statement generate a block inside the , and each entry is a TranslateString object. Which apparently imply that the translation is done at init time. Therefore, the callback receive a translated version of the string (confirmed this before answering).

It make it a little more difficult to patch translated games, but normally something like :
Python:
translate language python:
     toChange = {
        "Ceci est un texte." :
            "Non, ceci est un autre text.",
        "Oup's.":
            "Tout est correct.",
            }
should do the trick.
Which lead to a possible simplification of the RegEx part, to facilitate the use with translation :
Python:
    patchRegEx = r'(?:[Ll]andlady|[Hh]is father|Sarah)'
    regExEquivalent = {
        "Landlady": "Mother",
        "landLady": "mother",
        "His father": "My husband",
        "his father": "my husband",
        "Sarah", "mom",
        }

    def smtf( text ):
        def innerChange( m ):
            return regExEquivalent( m.group(0) )
        return toChange[text] if text in toChange else renpy.re.sub( patchRegEx, innerChange, text )
Then, now that nothing is effectively hardcoded, it can works with translated version of the text.


OK, now we just need somebody to ante a peso and a couple of pence, and we can kick off the poker game... LOL
Or we wait for girls and we start a strip one :D
 
  • Like
Reactions: Rich

khumak

Engaged Member
Oct 2, 2017
3,596
3,629
I'm kind of curious whether they only look at what options are available in game or whether they also make you rip out all associated code and visuals even if it's disabled.

The plan I have for a game I'm making is to set it up with incest included by default, including relationship names like son/daughter/mom/dad/brother/sister and also including all of the dialog, events, and visuals for incest content, but then also including some kind of incest fetish perk which is coded to be required for any related characters to get freaky with each other. Then have a little patch included with the game by default that simply disables that perk (and removes it from any sort of stat screen so you don't see any incest related perk). Then all you'd have to do to re-enable incest is just delete that file. Wouldn't work very well for a game where incest is the entire focus like Big Brother or something but for mine it's just 1 of many options.

So basically the game already includes the incest patch, you just delete the file to enable it. You could do something similar with overrides to add nicknames or something if you wanted I guess.

script.rpy ## just the relevant snippet for incest

Code:
label start:
    call motherfucker
    call rest_of_game
    return

label motherfucker:
    default incest = True
    return
relationship.rpy ##landlady patch (distributed with the game by default)

Code:
config.label_overrides["motherfucker"] = "mod_motherfucker"

label mod_motherfucker:
    default incest = False
    return
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,302
15,172
I'm kind of curious whether they only look at what options are available in game or whether they also make you rip out all associated code and visuals even if it's disabled.
So far, they seem to react on report. Therefore, they'll have something like: "This game have incest in it. If you do this/that, hop it appear". After it will depend of what "this/that" is.


The plan I have for a game I'm making is to set it up with incest included by default,
Don't. Every approach that just rely on a manipulation (file addition, file deletion, variable change) mean that you are making an incest game, period.

The only viable approach is a content change that is performed on an external file. And obviously this change must be to add the incest content, not to remove it.
 
  • Like
Reactions: khumak

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,576
2,204
Or put another way...

Assume you will get caught and that Patreon will look at your code (or someone will send a screenshot of it to them).

If your base game is squeaky clean... no problem. You can then go "Patch? What patch?"... and there's nothing to contest.
(I think it goes without saying, that if you do create a patch - don't mention it on any account connected to your Patreon, and create a separate, unconnected username on sites where you release the patch. In effect, make it look like someone else created it).

I know of one developer who released a patch completely OFF patreon... and patreon still went apeshit about it, because it was the same username... and therefore clearly the same dev.

Personally, I have my doubts as to whether Patreon ever go so far as to look at the source code. But then I don't have to rely on income from them. If in doubt, assume the worst and code your game accordingly.

The approach Anne suggests is probably the most practical solution. Have code which swaps one word or short phrase for another (make sure it's WHOLE words) and separately complete sentences/paragraphs too. Then wherever possible, swap words/phrases... and where that doesn't read quite right... swap the whole spoken paragraph for another.

And my favorite bugbear for text replacement... you may call your mom, "Mom"... but you'd call your ladylady, "Doris" or "Gina" or "Sofia" or "Paula" or whatever.
 
  • Like
Reactions: khumak

khumak

Engaged Member
Oct 2, 2017
3,596
3,629
I think I've settled on a method that minimizes hassle for me. Initially I'll write the dialog using incest since it's easier to wright mother, father, mom, dad, brother, sister, etc than inserting a bunch of variable names into the dialog like [landlady], [male_tenant], etc. But then similar to the method anne posted I'll do a find/replace all on those incest names and replace them with variables.

In my case I think it makes the most sense to just have a standard class for "nicknames" for every character in the game with the defaults being set to the sanitized, non blood relative name like landlady, tenant, roommate, etc. Then I'll have an in game screen where the player can just change anyone's name and all of their "nicknames" to whatever they want.

This allows the full range of usual relationship nicknames like mother, father, mom, dad, brother, sister, bro, sis, son, daughter, while also allowing every character to have some nickname for MC in particular. For instance sis might call mc a dork while the bully he just met might call him dumbass.

Since I'm only putting these in at the end, I won't have to remember any of these cryptic class variable names and they won't have anything incest sounding about them. I'll probably remove the comments for them as well so it will be even less clear what this part of the code is.


This is what I'm testing in my game now:

Code:
    class nicks:
        def __init__(self, mother, father, sister, brother, mom, dad, ssis, sbro, nmc):
            self.mother = mother # long nickname for landlady/wife
            self.father = father # long nickname for landlord/husband
            self.sister = sister # long nickname for female tenant/roommate
            self.brother = brother # long nickname for male tenant/roommate
            self.mom = mom # short nickname for landlady
            self.dad = dad # short nickname for landlord
            self.ssis = ssis # short nickname for female tenant
            self.sbro = sbro # short nickname for male tenant
            self.nmc = nmc # nickname for MC


# example definitions
    python:
        mc_n = nicks("landlady", "landlord", "roommate", "roommate", "old lady", "old man", "Jugs", "roomie", "me")
        marge_n = nicks("wife", "husband", "tenant", "tenant", "honey", "honey", "Lisa", "Bart", "Bart")
        homer_n = nicks("wife", "husband", "tenant", "tenant", "honey", "honey", "Lisa", "Bart", "Bart")
        lisa_n = nicks("landlady", "landlord", "roommate", "roommate", "old lady", "old man", "roomie", "Monkey", "Dork")
Edit: Tweaked my code again so that the code itself is initially easier to remember. I can easily just change the variable names to something sanitized at the end. The idea is for each "family" to be set up to include 1 old male and female, and 1 young male and female. So for instance using the same nickname system Marge can refer to Homer as her husband using the same nickname as what the kids use to refer to him as father or landlord.
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,302
15,172
I think I've settled on a method that minimizes hassle for me. Initially I'll write the dialog using incest since it's easier to wright mother, father, mom, dad, brother, sister, etc than inserting a bunch of variable names into the dialog like [landlady], [male_tenant], etc.
[...]
Edit: Tweaked my code again so that the code itself is initially easier to remember. I can easily just change the variable names to something sanitized at the end. The idea is for each "family" to be set up to include 1 old male and female, and 1 young male and female. So for instance using the same nickname system Marge can refer to Homer as her husband using the same nickname as what the kids use to refer to him as father or landlord.
Or you can take a look at this. It's old now, but normally it's relatively easy to use, while being also powerful enough to offer you enough flexibility. The advantage being that flexibility can be use as reason to use it if Patreon ask you something.
 
  • Like
Reactions: khumak