Writing Mods¶
Each mod has two components: the patcher and the additive game code. The patcher will do all the heavily lifting for modification of the game, and the additive game code will be anything that is new to the game.
To construct a mod, see the tree below.
MyMod
|-- __init__.py
|-- myResource.rpy
|-- resource
|-- custbg
|-- myBackground.png
Any *.rpy file is optional, and will be loaded automatically. The main mod file is mandatory for your mod to be loaded, and must be named __init__.py
. Every mod should start with from modloader.modlib import base as ml
. This will include all the utilities needed to get started. Using modlib
is as simple as calling functions on ml
. The resource
folder is optional as well. Files in this folder will be loaded into that game as if they were local to the game/ folder or the root of the archive. Should a file have the same name and location as one in the original game, the mod file will override the original.
To get full functionality from the modloader, it is recommended that you inherit from the modclass.Mod class. This will give you access to hooks at various stages during mod loading, and properly inform the user of your mod’s status. To do this, include from modloader.modclass import Mod, loadable_mod
at the top of your mod file. Define a class with a decorator as shown below:
@loadable_mod
class AWSWMod(Mod):
def mod_info(self):
return ("MyModName", "v0.1", "AuthorName")
def mod_load(self): # called immediately after loading. Most code should go here.
pass
def mod_complete(self): # called after all mods are loaded.
pass
However, to make a NSFW mod, it’s slightly different.
@loadable_mod
class AWSWMod(Mod):
def mod_info(self):
return ("MyModName", "v0.1", "AuthorName", True)
def mod_load(self): # called immediately after loading. Most code should go here.
pass
def mod_complete(self): # called after all mods are loaded.
pass
Please notice the tuple has a new fourth element that is True. If the fourth element is True, that indicates to the modloader to display a simple warning to people who want to enable or disable NSFW scenes.
Python and the Dangers of Rollback¶
Rollback is an integral feature of Ren’py, allowing the user to step back in time and make different choices. Usually rollback will play well with mods, but there are some cases that can introduce non-deterministic behavior. The main issue that rollback has is only Python objects encoded in Ren’Py’s AST(and then, only a select few statements) are rolled back. For this reason, Python functions should not be used in hooks if you can avoid them.
Steam Updates¶
The AWSW mod tools do not modify any of the core game files. They will survive and (probably) continue working through most changes to the game’s code. Hooking via the stable and recommended methods will ensure that a modification to the game’s code does not break your mod.
How to compile like a pro.¶
Instead of manually creating AST Nodes to insert code into the game, or even clunkily creating entries in the rpy language and then jumping to the snippets with the patcher, you can assemble small(or big!) bits of code in-line by abusing the Ren’py AST Parser. This means you can get a list of objects and insert them wherever with very little effort. We use this method internally to create the mod information menu on the main screen. See the core mod or the dev_test mod for further details.
Debugging¶
There are several obstacles to developing mods for Ren’py. The first(and biggest) is the fact that the developer has no console to watch for output while running the game! We can fix that by starting AWSW via command line with some switches set. The commands are below.
While in the root directory of the game, run the command for your appropriate operating system:
Windows¶
"lib/windows-i686/python.exe" -EO "Angels with Scaly Wings.py"
Linux x86 (32 bit)¶
./lib/linux-i686/python -EO "Angels with Scaly Wings.py"
Linux x86_64 (64 bit)¶
./lib/linux-x86_64/python -EO "Angels with Scaly Wings.py"
Should you print to the console in your code, you may run into an error such as LookupError: unknown encoding: cp437
.
This occurs because you’re executing a local version of python, which only has utf-8 string encodings loaded in. You can fix this by using sprnt
from modlib
or encoding your string with myStr.encode('utf-8')
before printing.
Sample Code¶
This sample code will remove Kevin’s encounter and main menu icon. The full mod structure can be found in mods/.
import renpy
import renpy.ast as ast
from modloader import modinfo
from modloader.modlib import sprnt
from modloader.modlib import base as ml # ml shortcut
from modloader.modclass import Mod, loadable_mod # Import the base class and the decorator
@loadable_mod
class AWSWMod(Mod):
def mod_info(self):
return ("byekevin", "v0.1", "")
def mod_load(self):
found = ml.searchPostNode(ml.findlabel("c4hatchery"), ast.Scene, 20) # search max of 20 nodes after c4hatchery label, look for the first scene initialization (which happens to be one opcode away, for now)
hook = ml.hook_opcode(found, None) # insert a hooking node after scene, but before the narrator's say statement
hook.chain(ml.searchPostNode(found, ast.Scene)) # normally the hooking node would point back to the dialogue. Skip all the way to the next scene instead.
mainscr = ml.getsls('main_menu') # get the main screen (cache is disabled)
ml.nullPyexpr(mainscr, 'persistent.playedkevin') # remove the if block
eHooks = ml.getEndingHooks()
true_search = eHooks.getPostTrueEndingIzumiScene()
def kevinCB(node):
if node.next is not None and isinstance(node.next, renpy.ast.Show) and node.next.imspec[0][0] == 'meetingkevin': # imspec is part of the image ID in show opcodes.
return True
kevin_credits = ml.searchPostNodeCB(true_search, kevinCB, 800) # search for a node using the kevinCB callback.
kevin_credits.chain(ml.searchPostNode(kevin_credits, ast.Scene)) # Show and with are separate instructions.
See the mods/ and devmods/ folders for further sample code. devmods/ contains a few different examples with fully annotated code.