Ren'Py Want to learn Ren'py modding

SAI HTI

New Member
Aug 23, 2017
3
0
Hi everyone,

I am a fairly new member who has been lurking in F95 community for a while. I am sure there are a few discussions about this topic regarding with modding tutorials but feels it would be better to get a fresh guide on learning how to mod existing Ren'py games.

To give an example, i am a big fan of Rogue-like: Evolution which is one of the most ambitious projects on here. and i do have some ideas in making new cosmetics for characters, new sprites and 2d illustrations for events, and a overarching dialogue options which is i think very difficult since i have to alter the game scripts.

So would it be possible to mod an existing game with new features like i mentioned above? I am a illustrator with beginner knowledge in C++ and Python. If possible, please provide me a guideline to mod games.

Thanks in advance
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,167
14,883
So would it be possible to mod an existing game with new features like i mentioned above?
The source code of any Ren'py game is fully available and so fully modifiable. What mean that everything is possible in term of modding.


If possible, please provide me a guideline to mod games.
Well, it's not really possible, for the reason gave above. Since you have full access to the source of the game, teaching you how to mod mean teaching you how to make a game with Ren'py.
Of course, there's alternate way to do, that don't imply alteration of the original code. But it's just methods use to hijack the code flow, or game screens, and to alter the text in real time. Therefore they do not fit the major changes you want to make.
 

Madmanator99

Member
May 1, 2018
225
452
I looked at that game Rogue-like: Evolution, and the latest version's files are packed in a .rpa file, so you'll first need to unpack them using Unren to get the actual script files, images, etc, the game resources basically.
Once you have done that, it's like anne O'nymous said, it's not an easy task. But, start by checking the script files, and see if you can start identifying the labels, variables, etc, and go from there.

The unren tool also allows you to enable the console, and you can then jump from label to label to further identify parts/sections of the game, without having to play the entire game, but also it may require that you set some variables, as they may be needed in some label (for example, must have at least 10 love points for "xyz" label to work, but if you jump to it while love points are 0, or not even defined, it will bug).
So everytime you jump to a label and you get a bug, check the corresponding script file, and see what variables/checks are needed/made, set them in the console, and keep going.

I guess the main point is that first, you need to have a good grasp of how the game is made that you want to mod, and then you can start changing/making mods for it.
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,559
2,177
As Anne says, it really depends on what techniques you want to use to mod the game.

Perhaps it would help if I explained my background in "modding" (I do not consider myself a modder).

As many here, I came across a single adult game on a different site (House Party). I started to look for more games. A couple of sites later and I ended up here on F95 and realized that this site was as interested in developers as much as distributing copies of the games. As a programmer myself (not python), that ticked some extra boxes for me, so I stuck around.

I alternated between RPGM and RenPy games by this point. Then once I discovered I could actually look at the RenPy source code using tools like UnRen, the programmer in me took more of an interest in that side of things.

By this point, I was noticing a lot of games were suffering from what I would call "Engrish". Just english enough to be recognizable, not english enough to flow properly. Most were poor google translations from other languages, but some were English speakers who were just typing too quickly to notice their own grammar and spelling mistakes (I do this A LOT myself, so no judgements from me - it's why proof-readers exist). I started to use UnRen to edit the source code of a few - mainly for my own sanity, but also so I could learn this new RenPy language a bit.

Then I came across Sister, Sister, Sister, a game that was almost right. There were a few spelling mistakes and grammar errors - enough to be manageable. So I edited my own copy to correct things as I went along and sent a list of the corrections by PM to the author (who didn't reply). A couple of new chapters were released and each time, I corrected stuff and PM'd the author the corrections.

By this point, this included the infamous "Truth or Dare" game in Chapter 6 - which actually crashed under some circumstances and was truly flawed in other ways. The author had clearly copy/pasted the code from somewhere else and "almost" got it working for his game. By this point, I knew just enough RenPy to apply my previous programming knowledge to fixing it - so my updated script files were now significantly different to the originals.

My changes were no longer single line spelling corrections - but whole chunks of code. I still hadn't heard back from the author - so I decided to post my updated script.rpy files to the game thread here on F95 - so other players could play an "improved" version of the game, without the minigame crashing. This alternate version felt more like "my" project now, so I went back and made some other dialogue changes that I'd felt uncomfortably altering when I was still trying to feed updates back to the author. Now, I was set free - able to make wholesale changes without feeling constrained to keep them small. My "" version, was just some replacement script files - which when placed in the game folder replaced the whole code with my own.

This is your first option for modding RenPy games. Replace the original script files with your own. If RenPy finds duplicated script files in a game folder (one in the .rpa archive and another cleartext version the same folder), it will use the newest version. Save files will be incompatible.

RenPy actually only uses .rpyc files when running. If it finds a mismatched .rpy file - it builds a new .rpyc file. Since that new .rpyc is newer, it is used instead.

Beyond this point, my changes to SSS became more sweeping. I started correcting images too, introducing new variables. My version no longer needed to be compatible with the original - it just had to deliver what the developer intended. It didn't help that by this point, the SSS developer was clearly getting more and more out of his depth. He was doing an excellent job, but making some mistakes which with a little more experience could easily have been avoided.

My "patched" version was much more of a fan-remake at this point - though I felt already committed to calling it a patch. I was also encountering more and more "your patch didn't work" problems - which were almost always due to . So I also started distributing a full version of the game as well as just the patch files to avoid such problems. Eventually I abandoned the patch files version entirely, once I realized that my version of the game wasn't compatible with the original - it was just easier for me to distribute the full version only and not need to deal with the support issues. Size concerns were solved by other people creating "crunched" version of my patched game.

This is your second option for modding. Replace the whole game with a version of your own. Call it a patched version, call it a fan remake. Whatever. Build a copy of "your" version using the RenPy launcher and distribute it in its entirety. Save files will be incompatible.

After SSS released it's final version, I decided not to do any more public modded versions without going through the original author. In effect, I was more interested in improving the original games than dealing with any support. I'd continue to play games and any that interested me enough to "correct" them while I was playing - I altered as I played and if I stuck with it, I'd eventually try to reach out to the author. If the original author didn't want to incorporate my suggested code changes into their game... that was fine. I do some work with the full knowledge that it might not be used... and if it was... well, that was just a bonus.

I scaled back my "fixes" a lot when I got involved in one starter project near the beginning. I did "corrections" for a couple of releases before it was noticed by the writer that I was "correcting" some dialogue that he didn't want corrected - including some in-jokes that I hadn't been aware of. At the very least, we'd each miscommunication what we thought we were doing. Regardless, it soured things a lot and I decided to walk away, knowing that the writer's contribution was needed a lot more than mine was. They'd also become popular enough by that point to attract other people willing to get involved in proof-reading the game. Still, the experience sucked some of the fun of helping people out it for me - so I do a lot less these days.

The problem with my approach so far has been that the resulting "improved" version is incompatible with the original. The advantage is that it's a lot easier than the alternative.

Which brings me to one of . Basically, someone asked for patch that would allow the player to use a custom MC's name, rather than use the one already coded into the game. I created a couple of patch files for the games he was interested in which used both of my final suggestions on how to patch games.

Firstly, there's something called config.label_overrides.

What this does is allow you to effectively rename existing labels. So imagine you create a new script file and the first thing it does is to rename label start: to label mynew_start: and then add a a brand new label start: to this new script. Now when the game starts, it will use the code in your new script file rather the original. Depending on what you are doing you could have the game execute the code in your new script and then jump to named alternative of the original label start: after it completes.

So this is your third options for modding. Effectively replacing one block of code with another using config.label_overrides.

If the original game rarely uses label {name}: statements, you may need to duplicate large sections of the original. But the resulting code is entirely separate from the original game and therefore reduces the chance that your save files will be incompatible. The larger your changes, the greater the chance that things will become incompatible.

Any label (and therefore section of the code) can be targeted. This method requires a greater understanding of what the original game does and how it does it. But end results are only limited by your own imagination.

You would do it with code like:
Python:
# file named something patch.rpy

default mc_name = "UnNamed"

init 3: # just making sure it's done after the usual init: code is completed.

    define config.label_overrides = {
        "start": "myrepl_start",
        "myrepl_start_END": "start"
        }

label myrepl_start:

    $ mc_name = renpy.input("What name would you like to use for this game?", length=20)
    $ mc_name = mc_name.strip() or "Alex"

    jump myrepl_start_END # jump to the original label start:, which is now known by this new name.

My final alternative is for when you just want to change individual lines of dialogue or wholesale effective "change all" type situations (like renaming a character, or using the word "Mom" instead of "Landlady"). It's not ideal when used wholesale, because sometimes some of the resulting sentences just won't make sense - but that's on you.

So this is my fourth option for modding. Effectively replacing one word/sentence/paragraph for another using config.say_menu_text_filter.

config.say_menu_text_filter allows you to specify a function that is called every time a bit of dialogue is shown in-game. The dialogue can be changed before it is shown (or not).

I borrowed some code Anne originally wrote for this. It includes two options.

Firstly, there is a list of whole lines of dialogue to replaced. Just a straight up replacement, nothing clever. Swapping one line with another. In this case, it was a single line of dialogue - but it could have been hundreds of changes - if that's what I needed.

The second part of the same solution was to use to replace individual words or phrases with something else. In my case, I was swapping the hardcoded use of the MC's name with the variable I'd added in the other part of my patch.rpy. I'm also changing potential references to a character "Emma" to her new name of "Saanvi".


Edit: 12/04/2021 : Slight edit to make the variable names a bit more user friendly.
Python:
init 3 python:

    wholeLineChg = {
        "{b}Alex' House{/b}" :
            "{b}[NameChar]'s House{/b}",
        }

    matchWordsChg = r'\b(?:Alex|ALEX|emma|Emma|EMMA)\b'
    replaceWordsChg = {
        "Alex": "[mc_name]",
        "ALEX": "[mc_name]",
        "emma": "saanvi",
        "Emma": "Saanvi",
        "EMMA": "SAANVI",
        }

    def smtf( text ):
        def innerChange( m ):
            return replaceWordsChg [ m.group(0) ]
        return wholeLineChg[text] if text in wholeLineChg else renpy.re.sub( matchWordsChg, innerChange, text )

    config.say_menu_text_filter = smtf

If you can follow this code, great. If not - just accept it tries to match stuff to the list in wholeLineChg first and then if that doesn't match tries to match it to the RegEx string called matchWordsChg ... if a match is found it uses replaceWordsChg as it's search and replace. If neither approach finds a match, the text is passed back unchanged.

There is a fifth option, which I've never tried to do stuff like this final option... which is to create a custom translation. Instead of Russian -> English or anything like that... it could be English -> Patched-English (or as I like to think of it Engrish -> English).

Lastly, there is the option to add entirely new code - not directly linked to the original code.
All you would need to do is write some new code in a new script file and then figure out a way to allow the player to interact with it. The two easiest methods would be to either add a keybind which triggers your new code or add something to the standard game's UI that triggers the new code. This approach is great for things like cheat mods and such. This could incorporate a variation of the "replaced label", where a replacement screen is written which does everything the original screen did and a little more besides... although there are ways of adding custom screen components to an existing UI without changing the original. Someone else will need to point you in the right direction though - if this is your preferred solution.
 
Last edited:

Madmanator99

Member
May 1, 2018
225
452
79flavors pretty much described most of the major things that can happen to a modder, to the letter.

I experienced the frustration of miscommunication alot in my line of work as well, and it is a major discouragement, and in particular when trying to modify a body of work that belongs to someone else. Nevertheless, the knowledge one gains during that time will serve in whatever endeavour you plan in the future. So it's not a total loss, far from it (after the emotional part dissipates). I myself have made mods for many games, and mods for game mods, and have faced such situations, but hey, I'm still doing it :)

Then the knowledge itself plays a part, and that is up to you, study and learn the code of the game you want to mod, it is your way in. And again, 79flavors pointed at the major ones.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,167
14,883
Wow, amazing reply, seriously.

[Note: I'll sometimes talk about my own mods. It's not to brag about them, but because they are practical examples, and the examples that, obviously, I know the most.]


This is your first option for modding RenPy games. Replace the original script files with your own. [...] Save files will be incompatible.
A precision on this point, because it happen often that, when a patch is released in a game thread to fix whatever issue with the current update, it's under this form. The save incompatibility isn't something that will happen every time. Therefore, it's generally safe to use one of these patches.

Here's a list, probably none exhaustive, of the potential problems with such method :
  • One or more variable are added, but not in a safe way ;
    The game will seem to works fine, until the moment Ren'py will have to proceed this variable in a way or another. See this for more on the subject.
  • A label is removed/renamed ;
    If the save file was created while inside this label, Ren'py while be unable to load it, unless it's at the very top of the label and the rollback stack permit it to fallback on a still existing label.
  • You changed the content of a label ;
    This isn't exactly a save incompatibility, but if the change happened in the label where the save file was created, and above the save point, then Ren'py will restart from the top of this label.
  • You don't just release the source, but the whole game as a Ren'py distribution ;
    If this distribution is made by using another version of Ren'py, then the compatibility isn't guaranty if you used a version prior to the one used by the game ; This happen(ed) regularly with compressed version of a game.
  • The rpyc files are rebuild from scratch ;
    This will happen if you deleted the said rpyc files and distribute the mod/patch with it or only as rpyc file. It will also happen if the original code is in a rpa archive.
    Be noted that, once again, this isn't exactly a save incompatibility, but Ren'py while lost his memory of what line have already be seen, preventing the skip feature to works as expected.
    Have to be added that, if the rpyc files are inside a rpa archive, you SHOULD provide a rpyc file with your mod/patch. Else, if the developer mode is unable, Ren'py will complain about duplicated labels, forcing the player to launch the game twice to be able to play it. An alternative to this, but it works only with Ren'py 7.2.0 and above, is to explicitly allow duplicated labels :
    Code:
    python early:
        config.allow_duplicate_labels = True
  • If you remove/rename a screen used by another screen still present ;
    Yet not exactly a save incompatibility since Ren'py will "simply" refuse to works. It will happen only if the developer mode is unable.
  • There's also issues if you remove/rename a screen, or change it to not use a given custom displayable.
    This is a weird problem that don't happen systematically. Normally Ren'py have no problems with this, but sometimes it just refuse to works, even if the screen wasn't shown when the save was created.
Be noted that all this don't apply only for modders and patchers. (Too) many game authors release updates matching one of those points.


This is your second option for modding. Replace the whole game with a version of your own. [...] Save files will be incompatible.
But if you did it right the save incompatibility will start only once the player started to play your version. Therefore, even if it's not necessarily recommended since you can have made changes prior to this point, a player can use his save files from the original game when playing your version for the first time.


I do some work with the full knowledge that it might not be used...
... but that you'll still learn a lot doing it. You in fact learn more that if you were working on your own project, because you have to understand what the original author did, and to works with his way to do it, that possibly isn't yours. Therefore, you quit was is your comfort zone, and start to use things that you never had to care for before.


Still, the experience sucked some of the fun of helping people out it for me - so I do a lot less these days.
Don't.
I don't talk especially about this person, but there will always be some "idiots" too entitled to their own works, or thinking that they can't be wrong. My own path crossed some of them, but... well, I did what I can, if they want to continue to have a bugged, impossible to maintain after a given amount of works, to hard or long to develop game, it's their problems, not mine.
It sometimes suck when the game was promising, but as I said, I did what I can, and so you did.


The problem with my approach so far has been that the resulting "improved" version is incompatible with the original. The advantage is that it's a lot easier than the alternative.
I tend to disagree with the "easier" thing.
Yes, creating my mod for Corruption needed a lot of time, around two weeks without counting the time to create the tools I use. It's more than what I would have had to use to do it by altering the original code. But once this initial works is done, I can release an update of the mod at most one day after the game was leaked here. Simply because I only need few hours to do it.
Unlike with an alteration of the original code, I don't have to care about the change prior to this version, whatever they are a text correction or an effective change into the code. This unless, obviously, it directly affect a part I modded. Which make my works way easier since I don't have to either apply my changes again to the original, or to import the changes made in the original into my version.
For this game there's even times where I just have to write: "Well, this update don't need that I also update my mod". Therefore, it took what ? 30 minutes of my time to looks at a diff of the update and previous version. This while a mod made by alteration of the original code would still need that you add the content of the update and release a new version of the mod.
I need more time for my mod for Super Powered, because the mod do way more complex things, but globally I still need less than 2 days while some of the mods for this game need a week to be updated.
As for my mod for Rogue Like: evolution, it's a really small thing, but apparently it still works more or less despite the fact that I haven't updated it since probably more than two years.

But this don't necessarily is a suitable option for everyone. Coding is my job, I started doing is when I was around 15 in the mid 80's, and at some time I had a formation in reverse engineering and used it at work. All this help me a lot to mod this way and make it natural for me to just "hook" to the original game.


Firstly, there's something called config.label_overrides.

What this does is allow you to effectively rename existing labels.
Or, perhaps more explicit for some readers, it's principal interest while modding is to hijack the game flow. While playing, the game will pass through your own label, in place of the label from the original code. Then you can, or not, decide to let Ren'py go back to the said original label. Which mean that either you totally changed the behavior of a label, or just added yourself on top of it.
The latter is one of the way I track seen scenes in my Super Powered mod. I use config.label_overrides to hijack the flow, and let my mod know that the player passed this point, then go back to the original label for the scene to be played. In short, I use it just to add a, "you've been here", information.


So this is your third options for modding. Effectively replacing one block of code with another using config.label_overrides.
I'm not impartial, but it's the option I recommend, unless the wanted changes are as big as those wanted by OP ; here it's mod a "fan version" than an effective mod.
This option present the advantage to be save compatible as long as you don't save the objects used by your mod ; so you need to be creative on the save part. You player can add your mod to the game without the need to restart a new game, as well as he can remove the mod and still continue without problem from his last save. He can use the mod with one update, then not the next one, to finally use it again latter, all this without problems if you did it right.


If the original game rarely uses label {name}: statements, you may need to duplicate large sections of the original.
Depending of what you want to do, it's not always needed. Ren'py translation feature permit to replace one dialog line by more than one dialog line, and there's way to totally remove (so not even have an empty say box) a dialog line without editing the original code. But the last option is more for advanced modders ; not that it's difficult by itself, but if you don't understand what you do, it can lead to problems.


[...] therefore reduces the chance that your save files will be incompatible. [...]
Which is the most important point that a modder should keep in mind. Whatever the way you mod a game, as long as it's just a mod and not a "fan version", the save files should be 100% compatible with an unmodded version of the game.


Any label (and therefore section of the code) can be targeted.
Totally not recommended if you don't totally know what you're doing, but this imply that you can also change Ren'py behavior since part of the core is wrote in Ren'py language.


This method requires a greater understanding of what the original game does and how it does it. But end results are only limited by your own imagination.
More globally, it's the whole Ren'py modding concept that is only limited by your own imagination.

As I said above, it's possible to totally remove a dialog line, and I used it in my Super Powered mod to change many of the in game notifications. They are initially a narrator dialog line, which mean that they are displayed on the "say box" and that the player have to click to advance the game. With the mod, they are notifications that appear in the top or middle of the screen, and automatically disappear after few seconds. As for what was "regular" notifications, they were renpy.notify kind of notification, and they now pill up, either at the bottom of the screen, or at the top of the "say box" depend if the said box is present of not. And all this without editing a single line of the original source code and 100% save compatible with the unmodded game.
It's not always easy to do, I needed a full week-end to achieve the dialog-like notification replacement, but globally speaking everything is possible as long as you have an idea regarding how if should be done. And if the none intrusive approach fail, you can fall back to the "fan version" and directly edit the original code.


Python:
[...]
    define config.label_overrides = {
        "start": "myrepl_start",
        "myrepl_start_END": "start"
        }
An advice for future, and some already established, modders: Whatever if you add a label or screen, hijack a label or screen, add a variable or a class, always prefer the use of explicitly prefixed, or suffixed, names. My variables, classes, labels and screens all are named "AONsomething" ; or put in an unreachable place, but it's something else. This way, the names will be unique and you'll be sure to never conflict with either the original code or another mod that happen to be compatible with yours.
It can be your initials like for me, or something obviously unique, like the "79" suffix used by 79flavors.

Oh, also, you don't need the define. config.label_overrides is already defined by Ren'py, so something like :
Code:
init 1 python:
    config.label_overrides["somelabel"] = "AONsomelabel"
would works fine.


config.say_menu_text_filter allows you to specify a function that is called every time a bit of dialogue is shown in-game. The dialogue can be changed before it is shown (or not).
And, as it name imply, it also apply for menu choice.
My mod for Bondage Island use it to add the location of each girl in the main navigation menu. So, it would be a long works, and sometimes need some additional computations, but it can be used for a none intrusive walkthrough mod by example.


I borrowed some code Anne originally wrote for this.
;)
You aren't the only one. Look at the, effectively unofficial, incest mods. There a little something that make obvious that at least half of them use my original code. I don't care, if I don't wanted it to be used, I wouldn't have gave it at first, but thanks for the credits.


Firstly, there is a list of whole lines of dialogue to replaced. Just a straight up replacement, nothing clever. Swapping one line with another. In this case, it was a single line of dialogue - but it could have been hundreds of changes - if that's what I needed.
Be noted that, if you fear that the list of lines to replace will be too big, you can take advantage of the config.label_overrides. Something that would looks like :
Python:
init 1 python:
    # Define a global list of changes to apply for the whole game.
    AONdialogChanges = { "this line": "become that one", [...] }

    # Define a list of changes dedicated to some labels.
    AONdialogChangesSpecial = {}

    # Update the callback to take count of the two lists.
    def smtf( text ):
        def innerChange( m ):
            return regExEquivalent79 [ m.group(0) ]
        # The special list take the priority.
        if text in AONdialogChangesSpecial: return AONdialogChangesSpecial[text]
        # Then if the dialog line wasn't in the special list, proceed as usual.
        return AONdialogChanges[text] if text in AONdialogChanges else renpy.re.sub( patchRegEx79, innerChange, text )

    # Hijack labels that will have a lot of dialog changes
    config.label_overrides["thisLabel"] = "AONthisLabel"
    config.label_overrides["AONthisLabelOUT"] = "thisLabel"

label AONthisLabel:
    # Define the dialog lines change that apply only for this label.
    $ AONdialogChangesSpecial = { "that line": "have to be changed",
         "this line also": "but differently" }
    # Then go back to the normal flow by playing the label.
    jump AONthisLabelOUT
 

SAI HTI

New Member
Aug 23, 2017
3
0
As Anne says, it really depends on what techniques you want to use to mod the game.

Perhaps it would help if I explained my background in "modding" (I do not consider myself a modder).

As many here, I came across a single adult game on a different site (House Party). I started to look for more games. A couple of sites later and I ended up here on F95 and realized that this site was as interested in developers as much as distributing copies of the games. As a programmer myself (not python), that ticked some extra boxes for me, so I stuck around.

I alternated between RPGM and RenPy games by this point. Then once I discovered I could actually look at the RenPy source code using tools like UnRen, the programmer in me took more of an interest in that side of things.

By this point, I was noticing a lot of games were suffering from what I would call "Engrish". Just english enough to be recognizable, not english enough to flow properly. Most were poor google translations from other languages, but some were English speakers who were just typing too quickly to notice their own grammar and spelling mistakes (I do this A LOT myself, so no judgements from me - it's why proof-readers exist). I started to use UnRen to edit the source code of a few - mainly for my own sanity, but also so I could learn this new RenPy language a bit.

Then I came across Sister, Sister, Sister, a game that was almost right. There were a few spelling mistakes and grammar errors - enough to be manageable. So I edited my own copy to correct things as I went along and sent a list of the corrections by PM to the author (who didn't reply). A couple of new chapters were released and each time, I corrected stuff and PM'd the author the corrections.

By this point, this included the infamous "Truth or Dare" game in Chapter 6 - which actually crashed under some circumstances and was truly flawed in other ways. The author had clearly copy/pasted the code from somewhere else and "almost" got it working for his game. By this point, I knew just enough RenPy to apply my previous programming knowledge to fixing it - so my updated script files were now significantly different to the originals.

My changes were no longer single line spelling corrections - but whole chunks of code. I still hadn't heard back from the author - so I decided to post my updated script.rpy files to the game thread here on F95 - so other players could play an "improved" version of the game, without the minigame crashing. This alternate version felt more like "my" project now, so I went back and made some other dialogue changes that I'd felt uncomfortably altering when I was still trying to feed updates back to the author. Now, I was set free - able to make wholesale changes without feeling constrained to keep them small. My "" version, was just some replacement script files - which when placed in the game folder replaced the whole code with my own.

This is your first option for modding RenPy games. Replace the original script files with your own. If RenPy finds duplicated script files in a game folder (one in the .rpa archive and another cleartext version the same folder), it will use the newest version. Save files will be incompatible.

RenPy actually only uses .rpyc files when running. If it finds a mismatched .rpy file - it builds a new .rpyc file. Since that new .rpyc is newer, it is used instead.

Beyond this point, my changes to SSS became more sweeping. I started correcting images too, introducing new variables. My version no longer needed to be compatible with the original - it just had to deliver what the developer intended. It didn't help that by this point, the SSS developer was clearly getting more and more out of his depth. He was doing an excellent job, but making some mistakes which with a little more experience could easily have been avoided.

My "patched" version was much more of a fan-remake at this point - though I felt already committed to calling it a patch. I was also encountering more and more "your patch didn't work" problems - which were almost always due to . So I also started distributing a full version of the game as well as just the patch files to avoid such problems. Eventually I abandoned the patch files version entirely, once I realized that my version of the game wasn't compatible with the original - it was just easier for me to distribute the full version only and not need to deal with the support issues. Size concerns were solved by other people creating "crunched" version of my patched game.

This is your second option for modding. Replace the whole game with a version of your own. Call it a patched version, call it a fan remake. Whatever. Build a copy of "your" version using the RenPy launcher and distribute it in its entirety. Save files will be incompatible.

After SSS released it's final version, I decided not to do any more public modded versions without going through the original author. In effect, I was more interested in improving the original games than dealing with any support. I'd continue to play games and any that interested me enough to "correct" them while I was playing - I altered as I played and if I stuck with it, I'd eventually try to reach out to the author. If the original author didn't want to incorporate my suggested code changes into their game... that was fine. I do some work with the full knowledge that it might not be used... and if it was... well, that was just a bonus.

I scaled back my "fixes" a lot when I got involved in one starter project near the beginning. I did "corrections" for a couple of releases before it was noticed by the writer that I was "correcting" some dialogue that he didn't want corrected - including some in-jokes that I hadn't been aware of. At the very least, we'd each miscommunication what we thought we were doing. Regardless, it soured things a lot and I decided to walk away, knowing that the writer's contribution was needed a lot more than mine was. They'd also become popular enough by that point to attract other people willing to get involved in proof-reading the game. Still, the experience sucked some of the fun of helping people out it for me - so I do a lot less these days.

The problem with my approach so far has been that the resulting "improved" version is incompatible with the original. The advantage is that it's a lot easier than the alternative.

Which brings me to one of . Basically, someone asked for patch that would allow the player to use a custom MC's name, rather than use the one already coded into the game. I created a couple of patch files for the games he was interested in which used both of my final suggestions on how to patch games.

Firstly, there's something called config.label_overrides.

What this does is allow you to effectively rename existing labels. So imagine you create a new script file and the first thing it does is to rename label start: to label mynew_start: and then add a a brand new label start: to this new script. Now when the game starts, it will use the code in your new script file rather the original. Depending on what you are doing you could have the game execute the code in your new script and then jump to named alternative of the original label start: after it completes.

So this is your third options for modding. Effectively replacing one block of code with another using config.label_overrides.

If the original game rarely uses label {name}: statements, you may need to duplicate large sections of the original. But the resulting code is entirely separate from the original game and therefore reduces the chance that your save files will be incompatible. The larger your changes, the greater the chance that things will become incompatible.

Any label (and therefore section of the code) can be targeted. This method requires a greater understanding of what the original game does and how it does it. But end results are only limited by your own imagination.

You would do it with code like:
Python:
# file named something patch.rpy

default NameChar = "UnNamed"

init 3: # just making sure it's done after the usual init: code is completed.

    define config.label_overrides = {
        "start": "myrepl_start",
        "myrepl_start_END": "start"
        }

label myrepl_start:

    $ NameChar = renpy.input("What name would you like to use for this game?", length=20)
    $ NameChar = NameChar.strip() or "Alex"

    jump myrepl_start_END # jump to the original label start:, which is now known by this new name.

My final alternative is for when you just want to change individual lines of dialogue or wholesale effective "change all" type situations (like renaming a character, or using the word "Mom" instead of "Landlady"). It's not ideal when used wholesale, because sometimes some of the resulting sentences just won't make sense - but that's on you.

So this is my fourth option for modding. Effectively replacing one word/sentence/paragraph for another using config.say_menu_text_filter.

config.say_menu_text_filter allows you to specify a function that is called every time a bit of dialogue is shown in-game. The dialogue can be changed before it is shown (or not).

I borrowed some code Anne originally wrote for this. It includes two options.

Firstly, there is a list of whole lines of dialogue to replaced. Just a straight up replacement, nothing clever. Swapping one line with another. In this case, it was a single line of dialogue - but it could have been hundreds of changes - if that's what I needed.

The second part of the same solution was to use to replace individual words or phrases with something else. In my case, I was swapping the hardcoded use of the MC's name with the variable I'd added in the other part of my patch.rpy.

Python:
init 3 python:

    toChange79 = {
        "{b}Alex' House{/b}" :
            "{b}[NameChar]'s House{/b}",
        }

    patchRegEx79 = r'\b(?:Alex|ALEX)\b'
    regExEquivalent79 = {
        "Alex": "[NameChar]",
        "ALEX": "[NameChar]",
        }

    def smtf( text ):
        def innerChange( m ):
            return regExEquivalent79 [ m.group(0) ]
        return toChange79[text] if text in toChange79 else renpy.re.sub( patchRegEx79, innerChange, text )

    config.say_menu_text_filter = smtf

If you can follow this code, great. If not - just accept it tries to match stuff to the list in toChange79 first and then if that doesn't match tries to match it to the RegEx string called patchRegEx79... if a match is found it uses regExEquivalent79 as it's search and replace. If neither approach finds a match, the text is passed back unchanged.

There is a fifth option, which I've never tried to do stuff like this final option... which is to create a custom translation. Instead of Russian -> English or anything like that... it could be English -> Patched-English (or as I like to think of it Engrish -> English).

Lastly, there is the option to add entirely new code - not directly linked to the original code.
All you would need to do is write some new code in a new script file and then figure out a way to allow the player to interact with it. The two easiest methods would be to either add a keybind which triggers your new code or add something to the standard game's UI that triggers the new code. This approach is great for things like cheat mods and such. This could incorporate a variation of the "replaced label", where a replacement screen is written which does everything the original screen did and a little more besides... although there are ways of adding custom screen components to an existing UI without changing the original. Someone else will need to point you in the right direction though - if this is your preferred solution.
Damn, it was very cool reading your experience and challenges with modding. To be honest, i do not understand some of the modding methods you have mentioned as currently i am very much a newbie with the nature of modding and Ren'py framework. Once i get familiar with the prerequisites then i can come back to this post and determine which workflow is suited best for what i am trying to do. Thank you really for putting up a lot of time and effort on writing this!
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,559
2,177
To be honest, i do not understand some of the modding methods you have mentioned as currently i am very much a newbie with the nature of modding and Ren'py framework. Once i get familiar with the prerequisites [...]

Thank you really for putting up a lot of time and effort on writing this!
No worries.

It's difficult to look into the details of how to do stuff without knowing the overall "feel" for what's possible. My post wasn't intended to be a "how-to", but more 1000ft overview of the type of approaches that could be used. The detail, as Anne points out, is a whole other beast. It's why I didn't explain save compatibility, despite mentioning it a couple of times. Better to know there's a minefield out there than be worried about each individual mine.

Likewise, the advance warning that not all your challenges will be technical.

My gut feel is that you probably need to be focusing on the config.label_overrides based solutions, but that will only become evident once you actually start messing about with actual code.

Good luck with your project.
 

SAI HTI

New Member
Aug 23, 2017
3
0
Thank you guys, for providing a lot of valuable insight for me and others aspired to mod. Now i know where and how to build foundation to mod. For starters i will now,
  • get familiar with Ren'py environment
  • learn Unren extractor & python
  • understand modding principles
This probably lacks depth and i am sure there are stuffs that needs to be covered as i went further into modding. I am committed to learning this as this would also benefit my career branch.

Again, thank you.
 

moskyx

Engaged Member
Jun 17, 2019
3,878
12,481
This is your first option for modding RenPy games. Replace the original script files with your own. If RenPy finds duplicated script files in a game folder (one in the .rpa archive and another cleartext version the same folder), it will use the newest version. Save files will be incompatible.

RenPy actually only uses .rpyc files when running. If it finds a mismatched .rpy file - it builds a new .rpyc file. Since that new .rpyc is newer, it is used instead.
That was a long yet interesting reading, mister. Just a lengthy question about this part, since I'm experiencing funny problems with my translation patches for games with rpa archives. Let's see if I can properly explain myself in English (for me it's confusing enough in Spanish, tbh).

Given that Ren'py translation tool need the rpy files to generate the translation files, I usually need UnRen to unpack those. But sometimes the rpy files aren't included in the rpa build. That's hardly a problem since UnRen can generate them from the rpyc files. Ok. Now that I have those rpy files, I need to edit them first as there are several strings (like character's names in character's definitions) that won't be extracted by Ren'py translation tool unless the function is used. And most devs never use it. Sometimes I even need the __(s) function, for text variables that appear on screen.

Fast forward to the end of the translating process. Right now, my game folder includes the original rpa build plus all the (edited) rpy files and their matching rpyc files (there's also a translation folder with the translated scripts, obviously, but you can forget that as it's not important for the matter in hands). Needless to say, the game runs perfectly in my PC. I can toggle languages and all texts appear translated as they should. According to your explanation, Ren'py is using the rpyc files extracted and updated after editing the rpy files. This allows the translation to work just fine.

Now I "build" my patch. I copy the tl folder and the game/script.rpy files, put them in a game folder and give it away. Then people install the patch in their own game folder. Right now, they have an original rpa archive (with the original rpy and rpyc files) and my edited rpy files. And here the fun starts.

When they run the modded game, Ren'py generate rpyc files from my edited rpy files. For some reason, this causes the translation to go crazy. The regular text (dialogs) displays untranslated, while the menu strings (basically evrything that goes with _(s) or __(s) function, plus all in-game menu choices) are translated
You don't have permission to view the spoiler content. Log in or register now.

So Ren'py is actually using the edited rpy files to create new and matching rpyc files, but somehow the translation is totally messed up. Why Ren'py aknowledges those strings (some of them actually introduced by my edited rpy files) but not the ordinary dialogues?

So I don't know the reasons. Fortunately, the solution is easy enough: just what anne O'nymous states here:

  • The rpyc files are rebuild from scratch ;
    This will happen if you deleted the said rpyc files and distribute the mod/patch with it or only as rpyc file. It will also happen if the original code is in a rpa archive.
    Be noted that, once again, this isn't exactly a save incompatibility, but Ren'py while lost his memory of what line have already be seen, preventing the skip feature to works as expected.
    Have to be added that, if the rpyc files are inside a rpa archive, you SHOULD provide a rpyc file with your mod/patch.
Acting Lessons cames with a bunch of rpa archives. Inside those rpa files there are both rpy and rpyc files. When I add my own rpyc files to the patch (so now the game folder includes the original rpa archives, my edited rpy files and the rpyc files created in my PC from those rpy files), translation works as intented.
You don't have permission to view the spoiler content. Log in or register now.

Funny enough, if I just include the tl folder, forgetting about the rpy and rpyc files, texts are correctly displayed in Spanish, except for the pop-up message that is NOT coded with the __(s) function in the original build. That could be expected, you might say, since Ren'py is running the original rpyc files and find matching translation files. But, surprisingly, character names, despite the fact that they aren't coded with the _(s) function, are also displayed in their translated form!
You don't have permission to view the spoiler content. Log in or register now.

So, my question is: What the heck is Ren'py doing when it finds rpa archives and rpy files?
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,167
14,883
When they run the modded game, Ren'py generate rpyc files from my edited rpy files. For some reason, this causes the translation to go crazy. The regular text (dialogs) displays untranslated, while the menu strings (basically evrything that goes with _(s) or __(s) function, plus all in-game menu choices) are translated
Which is perfectly natural.

Look at the translation files, you'll see things like that
Code:
translate french strings:

    # character.rpy:15
    old "head"
    new "Tête"

    # character.rpy:15
    old "chest"
    new "Poitrine"
that are for _() translation, and things like this
Code:
translate french bedsleep_cd611781:

    # wf "Good morning, sir"
    wf "Bonjour, monsieur"

# game/bedsleep.rpy:20
translate french bedsleep_e37e46b8:

    # wf "You got a package."
    wf "Vous avez un colis"
or this
Code:
# game/bedsleep.rpy:25
translate italian bedsleep_cd611781:

    # wf "Good morning, sir"
    wf "Buon giorno, signore."

# game/bedsleep.rpy:26
translate italian bedsleep_e37e46b8:

    # wf "You got a package."
    wf "C'è un pacco per lei."
that are for to dialog line translation.
[Note: Examples comes from The DeLuca Family]

As you can see, they aren't handled in the same way.

_() translation works by text comparison. Basically speaking the principle is the same that text = translation[language]["the text to translate"].
But for translation of dialog lines, it's different. The translation is handled directly by the dialog object. Basically speaking, during the game init phase, each dialog line store the translation associated to it, then when the said line have to be displayed, the code is similar in its intent to this one :
Code:
if language == "French":
   return frenchTranslation
elif language == "Italian":
   return italianTranslation
else:
   return notTranslated
Look closer at the second case. You'll see that both the french and italian translation use the same last parameter for each translate statement. This bedsleep_XXXXX parameter is a references to the element holding the line. But as I implied in my own answer, those element are rebuild each time a RPYC file have to be generated from scratch (so without the help of the actual RPYC file). And this imply that it is given a new reference, and so it break the translation mechanism.


But that's not important: as you see, quick menu options (that Ren'py codes with the _(s) function by default) appear in Spanish. Which is what I don't understand.
See above. The quick menu, and all default menus of Ren'py, are wrote to use the translation if it exist.
Code:
screen quick_menu():
[...]
             textbutton _("Back") action Rollback()
             textbutton _("History") action ShowMenu('history')
[...]
So, naturally, it use it when it exist. And it exist since you surely translated every single entry you found in the translation files.


So Ren'py is actually using the edited rpy files to create new and matching rpyc files, but somehow the translation is totally messed up.
Your error is the underlined part. As I implied previously, then explicitly said here, those files aren't matching.


Acting Lessons cames with a bunch of rpa archives. Inside those rpa files there are both rpy and rpyc files. When I add my own rpyc files to the patch (so now the game folder includes the original rpa archives, my edited rpy files and the rpyc files created in my PC from those rpy files), translation works as intented.
Because you used unren to extract the RPYC files from the RPA archive. Therefore, when Ren'py will build the new RPYC files, it will use the original ones as base, precisely in order to not mess with the references. But this don't happen when the RPYC files are in the RPA archive ; for Ren'py it's then a rebuild from scratch.


Funny enough, if I just include the tl folder, forgetting about the rpy and rpyc files, texts are correctly displayed in Spanish, [...]
Still because of the references that stay the same, since the RPYC files aren't rebuilt.
 
  • Like
Reactions: moskyx

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,559
2,177
It's less about .rpa and .rpy and more about .rpa and .rpyc.

Once it's got a valid .rpyc file, RenPy is happy. If it's got a .rpyc within an .rpa archive AND another .rpyc file outside - it will generally use the one outside, since it's almost always newer. I'm not sure if that's always true - or whether it's checking file dates, etc.

Personally, as soon as I unpack an .rpa archive... I delete it. It's (or they're) not needed any more.
Perhaps you've having some odd thing where you've got an .rpa within another .rpa archive once you build the translated project? I've no clue how that would behave.

What I do with projects I know I'm going to build again myself is:
  • Unpack the .rpa archives using UnRen
  • Delete the .rpa archives
  • If the .rpy files are missing, I'll use UnRen to recreate those.
  • Copy the contents of the /game/ folder to my RenPy projects folder.
  • Delete the script_version.txt file from the RenPy project folder (it causes problems if you leave it).
  • Edit the game
  • Build the game
The main thing to remember is to NEVER, EVER, EVER delete the .rpyc files.

The .rpyc contains using something called an Abstract Syntax Tree. In effect, the compile process numbers each line of code.

The numbering system is complex and is anchored to things like labels, etc.

The end result is that you can add lines of code or delete them or edit them... and even though the actual line number within the script file will have changed, it's AST reference won't have.

Minor changes (like adding an underscore "_" to the beginning of lines will likely not even cause the AST reference to be updated (RenPy is quite good at this sort of "minor change" decision making).

The AST reference is used by the game's history log, so that when you load a saved game that doesn't match the current version of the game - it backtracks through the AST references looks for a line it can match between the saved game and the current version of the game... and continues from that point instead.

It's why you can continue to update your game and the save files still work, even though you could have added/deleted/updated lots of lines of code.

If you delete the .rpyc file, the AST references are recreated from scratch - and therefore never matchs the original cross reference. Save files no longer work and everything goes to hell, even for a minor change.

I think the translations also link into the AST references too - so if you accidentally do something bad to the .rpyc, it will struggle to match the translations to the lines they are supposed to match.

Not sure if that helps you much though, except to say "delete the .rpa files once they're unpacked", "delete the script_version.txt" file and "never ever delete an .rpyc file"... then cross your fingers.
 

moskyx

Engaged Member
Jun 17, 2019
3,878
12,481
OK, thanks to both. Let's see.

I already knew dialog lines used that parameter, the AST mentioned by 79flavors, that changes almost every time you add a minor change in that line (from my experience I wouldn't say "almost" but always, as even the smallest typo fix forces me to translate the line again, as its AST has changed), but remains the same if you change its line-position within the file as long as you don't put it under a different label. And the _(s) action, as it's explained in the documentation, works differently as it displays a direct translation of the string (Ren'py kind of reads the text, instead of assigning it a parameter, and displays the translated string if available). And obviously if that _(s) is already coded in the original build, as in menus, the game will show the translation no matter if it's in a rpy, rpyc or rpa. Because it's a direct thing.

What I didn't know (and maybe I'm still not understanding it yet) is that the AST changes every time Ren'py has to build it. For some reason, maybe because of that sensitivity I mentioned above (a slight change in the line causes a completely different AST) I thought that the same line will always and under any condition get the same AST, and therefore same rpy file will generate same rpyc file. Somehow, it doesn't work like that. Every time Ren'py builds a rpyc, all the AST change even if the lines in the rpy file have not changed.

So, when extracting the rpyc files from the rpa in my PC, I'm getting the same rpyc built by the Dev, which have an assigned AST for each line. The game works with those AST and the translation matches because it was created with those AST (which are the same that players have in their own rpa archive, that's why copypasting just the tl folder seemed to work for the dialogues in my Acting Lessons example). But if I delete the rpyc file (or put just the rpy file in other PC, which is what players actually do) Ren'py will have to generate another rpyc file which will be different to the original within the rpa, even if the base rpy is still the same, and obviously the translation won't match because its AST are different. Am I right?
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,167
14,883
What I didn't know (and maybe I'm still not understanding it yet) is that the AST changes every time Ren'py has to build it. For some reason, maybe because of that sensitivity I mentioned above (a slight change in the line causes a completely different AST) I thought that the same line will always and under any condition get the same AST, and therefore same rpy file will generate same rpyc file. Somehow, it doesn't work like that. Every time Ren'py builds a rpyc, all the AST change even if the lines in the rpy file have not changed.
AST entries are objects representing the statement at "this position of the script", and nothing else. Therefore, because of Python's immutability, Ren'py could potentially reuse objects for statements that haven't changed, but for those that have changed there's no other choice than creating a new one.
But what really matter here is the internal ID of those objects. Basically speaking, when a RPYC file exist, Ren'py will try to copy it as much as possible ; not just because of translation issues, but also because the whole skip feature, rollback feature, and even the save system, rely on those internal IDs. But obviously, if Ren'py don't find a RPYC file, it have nothing to copy, and therefore each object will have a totally new ID that have really few chance to be the same than previously.


Am I right?
You are.
 
  • Like
Reactions: moskyx

moskyx

Engaged Member
Jun 17, 2019
3,878
12,481
AST entries are objects representing the statement at "this position of the script", and nothing else. Therefore, because of Python's immutability, Ren'py could potentially reuse objects for statements that haven't changed, but for those that have changed there's no other choice than creating a new one.
But what really matter here is the internal ID of those objects. Basically speaking, when a RPYC file exist, Ren'py will try to copy it as much as possible ; not just because of translation issues, but also because the whole skip feature, rollback feature, and even the save system, rely on those internal IDs. But obviously, if Ren'py don't find a RPYC file, it have nothing to copy, and therefore each object will have a totally new ID that have really few chance to be the same than previously.
Thanks a lot, really. You two are so helpful, I learn a lot reading your replies to people who ask for help even if sometimes I don't quite get what are you talking about.

In this case, what I still don't get is this "internal ID" you mention. Reading a bit more about the AST system, I assume is kind of an encryption system because, for a given content, it creates a cyphered version of it that contains the basic info to make the program run (what are the text to be shown and the actions to perform when executing that encrypted line, and how it relates to other parts of the script). Obviously, if I introduce a change in a line in the rpy file, the cyphered version of that line also changes in the rpyc, this invalidating everything that uses the former version as reference, like save/rollback points, translation strings and so on. For save/rollback points Ren'py can find the previous unchanged statement and goes there, and for translations that line just stops working and you need to "manually" fix it by offering a translation that matches the new cyphered version. So far so good. But an AST also allows a "decryptation" so that, if you only have the cyphered message (the rpyc file) you can get the original message (the rpy file) like UnRen does when the rpy files are not compiled into the rpa archive.

And then I'm still struggling to understand why two identical rpy files can "give birth" two different rpyc files which would mess up all the Ren'py functionalities that rely on that "internal ID". As I said, I get the rpy from a rpyc compiled in a rpa, so it's easy to assume this rpy file is the same created by game's Dev. If I put that very same rpy outside the rpa, without the rpyc, everything goes to hell. Shouldn't those rpyc files, both the new and the old one contained in the rpa, be the same? Why their respective AST are built differently?

Do you know how this 'building rpyc from scratch' process works? It implies some randomity, or I'm completely lost and there's an absolutely different reason why? Like using time creation data or something like that? As you might have notice at this point, I don't have any programming education
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,167
14,883
In this case, what I still don't get is this "internal ID" you mention. Reading a bit more about the AST system, I assume is kind of an encryption system because, for a given content, it creates a cyphered version of it that contains the basic info to make the program run (what are the text to be shown and the actions to perform when executing that encrypted line, and how it relates to other parts of the script).
It's simpler than that. It's a value based on the file name and a "magic number" unique for the given line. But the spirit is the same. Whatever how the ID is made, the goal is just to ensure that there will be one and only one version of a given ID, for each one to be unique for a given game. This way, latter Ren'py just have to address this ID to retrieve the right entry in the AST.

Imagine that you've this AST:
  1. scene something
  2. "blabla"
  3. "blublu"
  4. "blibli"
  5. if condition:
    • jump somewhere
  6. jump somewhereElse
Then you make some change, and the new AST looks like that :
  1. scene something
  2. "blibli"
  3. if condition:
    • jump somewhere
  4. jump somewhereElse

Lets say that you play with the first version, and save at the line 4. If the save was based only on the position in the AST, then a load with the new version of the script would start at the line 4, which is jump somewhereElse, totally skipping the if.
But with the internal ID, when you load, Ren'py search for the right ID. It will find it at the line 2 "blibli" which is where you effectively saved, and restart from this point. And if you kept the line "blabla" in place of the "blibli" one, failing to find the right ID, Ren'py would have restarted at the top of the label ; this to ensure that the player will miss nothing.


But an AST also allows a "decryptation" so that, if you only have the cyphered message (the rpyc file) you can get the original message (the rpy file) like UnRen does when the rpy files are not compiled into the rpa archive.
The difference being that RPYC files aren't at all cyphered. They are just the "binary form" of the code once in memory. Unren works because each Ren'py statement have its own object and contain all the information needed to reverse it.
If you want, you can see RPYC files as a snapshot of the memory for each files, since it's basically what Pickle do.


And then I'm still struggling to understand why two identical rpy files can "give birth" two different rpyc files [...]
Specifically because the ID isn't a cyphered version of the line ; it would need too much time and generate a too big value. I don't remember exactly how the "magic number" is computed, but imagine that it's something like that : str( timestamp + number of characters + CRC of the line ) + str( line number )
This would return a value unique even if Ren'py process hundreds of line each second, and so have the same timestamp. This because number of characters + CRC of the line would be relatively unique, and the whole thing is completed by the line number, which is unique for a given file.
But once you have to generate the number again, this time it's the timestamp that is now the radically different part, what will lead to a totally new "magic number".
 
  • Like
Reactions: moskyx

moskyx

Engaged Member
Jun 17, 2019
3,878
12,481
It's simpler than that. It's a value based on the file name and a "magic number" unique for the given line. But the spirit is the same. Whatever how the ID is made, the goal is just to ensure that there will be one and only one version of a given ID, for each one to be unique for a given game. This way, latter Ren'py just have to address this ID to retrieve the right entry in the AST.

Imagine that you've this AST:
  1. scene something
  2. "blabla"
  3. "blublu"
  4. "blibli"
  5. if condition:
    • jump somewhere
  6. jump somewhereElse
Then you make some change, and the new AST looks like that :
  1. scene something
  2. "blibli"
  3. if condition:
    • jump somewhere
  4. jump somewhereElse

Lets say that you play with the first version, and save at the line 4. If the save was based only on the position in the AST, then a load with the new version of the script would start at the line 4, which is jump somewhereElse, totally skipping the if.
But with the internal ID, when you load, Ren'py search for the right ID. It will find it at the line 2 "blibli" which is where you effectively saved, and restart from this point. And if you kept the line "blabla" in place of the "blibli" one, failing to find the right ID, Ren'py would have restarted at the top of the label ; this to ensure that the player will miss nothing.


The difference being that RPYC files aren't at all cyphered. They are just the "binary form" of the code once in memory. Unren works because each Ren'py statement have its own object and contain all the information needed to reverse it.
If you want, you can see RPYC files as a snapshot of the memory for each files, since it's basically what Pickle do.


Specifically because the ID isn't a cyphered version of the line ; it would need too much time and generate a too big value. I don't remember exactly how the "magic number" is computed, but imagine that it's something like that : str( timestamp + number of characters + CRC of the line ) + str( line number )
This would return a value unique even if Ren'py process hundreds of line each second, and so have the same timestamp. This because number of characters + CRC of the line would be relatively unique, and the whole thing is completed by the line number, which is unique for a given file.
But once you have to generate the number again, this time it's the timestamp that is now the radically different part, what will lead to a totally new "magic number".
Thanks, I think I see the difference now. Today, the rpy files might be exactly the same, but when the first rpyc was created the original rpy looked different. When compiling the rpyc, Ren'py assigned some values to certain parts of that rpy that now can be somewhere else (or could be entirely gone), but those values won't change in order to allow a quick identification. So if the program needs to create a new compilation today, the AST will be neccesarily different.

I also took a look at the "renpy" folder and saw the two different sets of functions used for translation purposes, one for "strings" (menus, buttons, variables and so on) and the other for dialogues. And the dialogue's one requires an AST to identify the blocks of text prior to give a MD5 encryptation code to each line (then it just scan the text looking for those codes and replace the lines that match). Now I guess that, while those MD5 codes (the parameter you talked about the other day) won't change if the 2 rpy's file are the same, as they only depend on the label and content of each rpy line, the functions that actually extract those dialogue lines to translate and display the translated ones need the AST to be the same.

Again, thanks for your dedication
 

Meushi

Well-Known Member
Aug 4, 2017
1,146
12,708
This is great stuff, learning a lot from these sorts of posts.

This option seemed useful for correcting typos, as shouldn't break saves & more compatible with other mods like walkthrough/cheats:
So this is my fourth option for modding. Effectively replacing one word/sentence/paragraph for another using config.say_menu_text_filter.
I thought I had a basic grasp of how it works (couple of dictionary lookups, if finds the key phrase in either dict returns the associated value), so tried to do this for the latest update of Rebirth. Problems ensue...

Python:
init 1 python:

    toChangeMi = {
        "archon": "Archon",
        "She pushes the shaft of her axe againt his chest.": " She pushes the shaft of her axe against his chest.",
        "I can see where this si going.": "I can see where this is going.",
        "No need to antagonize anyone by you been seen at the archon's": "No need to antagonize anyone by you being seen at the Archon's",
        "call you when it's get close to sunrise": "call you when it gets close to sunrise",
        "I doubt he's not lying": "I doubt he's lying",
        "And reevaluate the landscape": "And re-evaluate the landscape",
        "You don't take me as the dim type, Sharon": "You don't strike me as the dim type, Sharon",
        "You fist clenches involuntarily": "Your fist clenches involuntarily",
        "Do now address her as such!": "Do not address her as such!",
        }

    patchRegExMi = r'\b(?:Zephir|zephyr)\b'
    regExEquivalentMi = {
        "Zephir": "Zephyr",
        "zephyr": "Zephyr",
        }

    def smtf( text ):
        def innerChange( m ):
            return regExEquivalentMi [ m.group(0) ]
        return toChangeMi[text] if text in toChangeMi else renpy.re.sub( patchRegExMi, innerChange, text )

    config.say_menu_text_filter = smtf
My problem is none of the key: values in toChangeMi are being replaced in game (i.e. archon is not being corrected to Archon etc.).

However, the key: values in regExEquivalentMi are being corrected in game (i.e. Zephir and zephyr appear as Zephyr).

Any pointers on what I'm messing up would be appreciated.
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,167
14,883
My problem is none of the key: values in toChangeMi are being replaced in game (i.e. archon is not being corrected to Archon etc.).
toChangeMi MUST have as key the whole dialog line, character for character, and in the default language of the game. I can't say for the others entries, but I'm sure that "Archon" don't correspond to this. When you want to change only a part of the line, or just a word, it's the couple patchRegExMi and regExEquivalentMi that you have to use.
But I recommend to limit this use to the minimum. RegEx are relatively fast, even in Python, but obviously the longer is the motif (the string describing the RegEx), the slower it will be. Therefore, prefer to use it when there's just a word, preferably recurrent, to change, and use toChangeMi for the rest, even if you want to just change a part of the line.
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,559
2,177
Yeah.

Just to reiterate what Anne said... toChangemi is for whole dialogue lines... word for word... letter for letter... They need to be exact matches for the whole line, including any text tags like [b] or [i].

Whereas patchRegExMi and regExEquivalentMi are for whole words or phrases which are part of a larger line.
(the \b \b within the regEx definition is what limits it to whole word boundaries, and is there to stop things like Ann -> Anne resulting in "Happy Anneiversary").

So I would expect "archon" to be in patchRegExMi and regExEquivalentMi at the least.

Also... your first text replacement, "She pushes the shaft of her axe againt his chest.". The actual line in the original code starts with a space - so your search/replace needs to also include that space, or it won't be an exact match.

Likewise, the second search and replace is "\" I can see where this si going.\""... you'll need to include those escape codes too.

Somewhat off topics...
... and it's a bit late now, but perhaps for the next game, the author could include character definitions like:
Python:
define l = Character('Laurie', color="#ff6666", image ="f2", what_prefix="\"", what_suffix="\"")
(and therefore avoid all that extra typing to add double quotes in everywhere manually).

The technique is also handy for "character thinking" type definitions too, where you might code something like:


Python:
define p = Character('[player_name]', color="#ff6666", what_prefix="\"", what_suffix="\"")
define p_t = Character('[player_name] *thinking*', color="#ff6666", what_prefix="[i](", what_suffix=")[/i]")
This allows for spoken text to be wrapped in double quotes and for character's thinking to be shown in italics and wrapped within parenthesis ( ). I'd also be tempted to have character thoughts be shown in a slightly grayer text rather than pure white by adding , what_color="#CCCCCC" to the "thinking" character definitions.

cc: Cpt. Arthur Hastings - FYI.
 
Last edited: