Codes: Censoring Nudity (the YAGS way)

One of the things I’ve been more aware of recently is nudity in games, and particularly how that plays (or doesn’t play) with payment processors.

A lot of games, to stay on the safe side, have released their games in a “censored” mode and then released patches to re-enable nudity. I’m not sure how other people do it, but my goal was to make it as simple as possible for users (one file to replace) but also give myself a lot of control over changes.

As always, I don’t pretend that this is the right or best way to do things… only that it works for me.

First off, you have to identify what file(s) and game declarations have nudity or will be affected by censoring. In my case, I have both nude sprites and nudity in my CGs, located in /images/chnew and /images/cg, as well as LayeredImage declarations for my CGs.

We start by defining a new rpy file ndpatch.rpy to hold the aspects that require and control patching. Because of the way my sprites work, the only other thing I need in here is the CG definitions*. We also include some additional constants, for use later.

## Is nudity allowed at all
define nudityIsAllowed = True

##  is for uncensored, c is for censored
define nudityverending = ""

## Align the game with the patch version where necessary
## Update expectedndpatchversion in options.rpy
define ndpatchversion = 1

layeredimage person_cg:
    always:
        "cg/person_base.jpg"
    if persistent.bodyhair_person:
        "cg/person_hair.png"
    if not persistent.nudes:
        "cg/person_censor.png"

Next, we define a separate rpa file for everything related to nudity (the sprites, the CGs, and the ndpatch.rpy file itself). This should come first in the build.classify calls since each file will be placed into an archive based on the order the rules are declared, in options.rpy. We also add a corresponding constant that will not be built into ndpatch.rpa.

## Match the patch version in ndpatch.rpy
define expectedndpatchversion = 1

init python:
    build.archive("ndpatch", "all")
    
    # Nudity
    build.classify("game/ndpatch.rpyc", "ndpatch")
    build.classify("game/images/chnew/**", "ndpatch")
    build.classify("game/images/cg/**", "ndpatch")

    # ...other archive and classify calls...

Now, we make a copy of the entire game project. In that copy, we edit the rpy file to reflect censored nudity.

## Is nudity allowed at all
define nudityIsAllowed = False

##  is for uncensored, c is for censored
define nudityverending = "c"

## Align the game with the patch version where necessary
## Update expectedndpatchversion in options.rpy
define ndpatchversion = 1

layeredimage person_cg:
    always:
        "cg/person_base.jpg"
    if persistent.bodyhair_person:
        "cg/person_hair.png"

And then we also make sure, in the censored version of the project, we merge the censor in the CGs with the base layer (so even the raw game assets don’t have nudity that can be uncovered via unarchiving the rpa file later). Similarly, any sprite image files that have nudity in them must be edited in the censored version of the project to remove or obscure nudity.**

Technically, this is enough to get censoring working. You would simply build a distribution of the game via the censored version of the project, build a second distribution via the uncensored version, and provide the ndpatch.rpa file from the uncensored version for download. The images will be replaced in the game with uncensored versions when the ndpatch.rpa file is replaced, since they are part of the ndpatch.rpa file.

However, we can do a little better.

First, you can use the nudityverending constant to change how the version of your game displays, to make it obvious whether it’s got nudity or not. For example:

screen about():
...stuff...
text "Version [config.version!t][nudityverending]"
...stuff...

Second, your game may later need to change in a way that makes an older ndpatch.rpa incompatible. For example, if you add a new sprite expression, someone with an older ndpatch.rpa file that tries to use it against a newer game build will see errors when running the game. Similarly, if you delete an asset, someone with a newer ndpatch.rpa file will see errors with an older game build.

This is where the ndpatchversion and expectedndpatchversion constants come in handy. You can ensure they are equal when the game starts up and show an error or warning if they don’t match. For example:

label start:
if ndpatchversion != expectedndpatchversion:
    "Your ndpatch.rpa file is the wrong version for this build of the game."
    "Please download the latest patch and game from URL."
    return

and

screen main_menu():
    fixed:
        if ndpatchversion > expectedndpatchversion:
            text "{size=50}{color=#000}Your ndpatch.rpa is newer than the build of this game (v[config.version]). Please download the latest version of the game before patching.{/color}{/size}" xsize 500 xanchor 0.0 yanchor 0.0 pos(550, 100)
        elif ndpatchversion < expectedndpatchversion:
            text "{size=50}{color=#000}Your ndpatch.rpa too old for use with this build of the game (v[config.version]). Please download the latest version of the patch and try again.{/color}{/size}" xsize 500 xanchor 0.0 yanchor 0.0 pos(550, 100)

which then lets you bump both constants whenever you make a breaking change (like adding or removing a sprite asset or cg).

Finally, you can use the nudityIsAllowed constant inside the game itself anywhere that you want to handle differently in a censored vs uncensored game. For example, I change the warning that shows at the beginning of the game to reflect explicit vs non-explicit nudity.

There’s probably a way to make this work with a single Renpy project that builds both a ZIP distribution and a separate ndpatch.rpa file. But it seemed easier to me to just duplicate the project. But because of this, you probably want to do this work when your game is otherwise done, or you’ll potentially have to make bug fixes in both copies of the game project.


* Yes, I could have left the CG declarations outside of ndpatch.rpy and simply censored the base image, but that would require me to have a cg/person_censor.png file in both versions of the game, and that felt inefficient for me.

** Identifying and editing relevant images is where most of the work is, really. If you just want to flat out replace images, you don’t even need an ndpatch.rpy file. Simply declaring an ndpatch.rpa archive in options.rpy, and including all image files that will need to be censored in that archive, would be enough.

Leave a comment

Your email address will not be published. Required fields are marked *