@4onen: Blog

Mod Load Order

4onen

2024-08-10

Introduction

Recently in the AwSW community I have noticed some confusion about the order in which the Ren’Py game engine, augmented with our community’s wonderful core modtools, loads mods. There’s a lot to unpack, from compatibility issues to unexpected gameplay changes. I’ll be breaking down the importance of load order for AwSW mods, outlining the common pitfalls, and offering best practices to ensure a smooth and enjoyable experience.

What is a Load Order?

A load order is the sequence in which mods are loaded into the game. This sequence determines how the game processes and applies the changes made by each mod. The order in which mods are loaded can significantly impact the gameplay experience, as mods can conflict with each other or overwrite each other’s changes if not loaded in a compatible order.

Let’s consider a few scenarios:

The Ren’Py Load Order

Angels with Scaly Wings ships with the Ren’Py game engine version 6.99.12.2.2029, which is available at their GitHub repository, commit 183327eec. It’s this code I’m referencing when I talk about load orders in AwSW. Future versions of Ren’Py likely differ.

Script File Loading

The Ren’Py game engine, during startup, produces and caches a scan2 of all the game files on its “search path.” This path includes the game directory, the game archive, and any directories specified in the config.searchpath variable. The script modules (and their compiled bytecode) are then copied from this cache into a list3, which is then sorted by the path of each file relative to the search path directory it was found in.4 Normally this would produce an order that is inconsistent between different systems, as Windows uses \\ as a path separator while Linux uses /. However, Ren’Py normalizes all paths as it caches them.5

The game will search for both rpy and rpyc files at each path, and compile and load them in this sorted order. Notably, the rpyc file takes precedence, unless the game is in a full-recompilation mode or the MD5 hash of the rpy file does not match the hash stored at the end of the rpyc file.6 This is why Ren’Py can take a while to load as players add more, larger mods. Ren’Py is taking the MD5 hash of every rpy file to check if it needs to recompile it. The more mods added, the more files it has to check, since the modding community has chosen to distribute mods as source code.

Init Phases

After loading the script files, Ren’Py will collect all the init blocks from the script files into a list called initcode, which is then sorted stably by priority.7 This priority is an attribute of every init block that determines the order in which they are executed. Negative priorities are executed first, then zero, then positive. Because we sort the list stably, if two init blocks have the same priority, they will be executed in the order they were found in the script files. Within one script file, this means they execute in order as you go down the file. Between script files, this means they execute in the order the files were loaded, described above.

Init phases that are allowed for users to manipulate in their game range from -999 to 999. The default init phase for an init block is 0, which is the init phase used by the majority of AwSW.

Mods are loaded during init phase 0, so whenever the modloader/bootstrap.rpy file appears in the list of script files. This falls in the midst of the game’s own init blocks, before all files alphabetically sorted after modloader and after all files alphabetically sorted before modloader. As modloader sorts before mods, the modtools run and import mod configurations before any mod scripts with init priority 0 are have run, but after the Ren’Py game engine has loaded all script files and solidified their load order. Any negative priority init blocks have already run. Any positive priority init blocks will run after the modtools have finished their loading process.

The Modtools Load Order

All of the above applies to Ren’Py script files and their compiled bytecode, those in rpy, rpyc, rpym and rpymc formats. The modtools and mods built with them, however, are written in Python and are loaded as Python modules. These python modules from Ren’Py’s perspective are all loaded the moment the modloader/bootstrap.rpy file gets to run its init 0 phase. So during that blink of time from the game engine’s perspective, what’s actually happening?

Code in this section of the post is referencing this commit of the AwSW modtools repository.

Discovering Mods

The modtools first discover all the mods in the game/mods directory by listing its contents. Importantly, this list is not sorted in any way; it is the order the operating system lists the subdirectories in the mod directory. This means that the order in which mods are discovered is not predictable and can vary between systems. This is why it’s important to avoid relying on the order of mod discovery for any import statements or other code required just to load your mod’s configuration.

If any non-folders are found in the game/mods directory, the modtools will raise an error and halt the game. This is to prevent any accidental inclusion of files that are not mods, such as packed zips or incorrectly unpacked loose files.

Importing Mod Configs

Once the list of mods is discovered, the modtools will import each mod’s __init__.py file. This executes all code in the file top to bottom, including import statements, variable assignments, and function+class definitions. It is expected that each mod will include exactly one class definition that inherits from modclass.Mod (typically named AWSWMod) and has the decorator @modclass.loadable_mod. This is how the modtools know where to get the mod’s configuration from, and how to load the mod’s scripts.

Each modclass.Mod subclass must have the following defined to be loaded successfully:

Dependency resolution

Each mod’s subclass of modclass.Mod may also define a dependencies class variable that is a list of strings. These strings come in three different forms:

An example dependency list might look like this:

dependencies = [
            "MagmaLink",
            "?Side Images",
            "!My Cool Game-Breaking Mod",
        ]

The order of dependencies in this list is not important, nor is it preserved in any way – the list is treated like a set of dependencies. The modtools will then topologically sort the mods based on their dependencies, ensuring that mods that have no dependencies are loaded first, followed by mods that depend on those mods, and so on. If a cycle is detected in the dependency graph, the game will raise an error and halt.

The implementation of the topological sort is an unstable O(N^2), which is performed on the already-unstable order that mods were discovered. This means that unless a dependency relation is explicitly declared, no ordering of mod loading can be strictly guaranteed. If a mod intends to have any cross-mod functionality, it should declare the other mod(s) as at least optional dependenc(ies) to ensure the other(s) load(s) first.

mod_load

The mod_load method of each mod is called in the order the mods were topologically sorted. This means that mods with no dependencies are loaded first, followed by mods that depend on those mods, and so on. This is where the mod should set up any scene changes, register any new screens, or perform any other setup that should happen before the game starts.

Many mods depend on a mod called MagmaLink, which provides a framework to massively ease the process of manipulating the game’s scenes. All functionality relating to MagmaLink should typically be performed in this mod_load method, as MagmaLink is not guaranteed to be loaded prior to this point, and most mods expect game scene manipulations to be complete by the time their mod_complete method is called.

mod_complete

The mod_complete method of each mod is called in the order the mods were topologically sorted, after all mods’ mod_load methods. This is where the mod should perform any final setup that requires all mods to be loaded. Before dependency resolution was added to the modtools, this was the only place where mods could be sure that all other mods were loaded. Now, this method is largely vestigial, but it is still a required definition for each mod to have.

Some mods still choose to wait until here to load their “Side Images” or other optional mod assets.

MagmaLink is a mod that provides a framework for other mods to manipulate the game’s scenes. One of the ways it does this by providing a series of “scene builders” that know how to manipulate some of the game’s most complex scenes without breaking them, such as the Answering Machine scene or the character selection screens. These scene builders are like forms that each mod fills out, asking for certain changes to these scenes.

Because MagmaLink must be fully loaded before any mod can use its scene builders, MagmaLink can’t have the scene builders actually apply their changes at that time. As it’s still technically legal to fill out the scene builders in mod_complete (albeit bad practice,) MagmaLink also can’t apply the changes in its own mod_complete method. Instead, MagmaLink waits until the init 999 phase to apply the changes specified by the scene builders. This is the last phase of the game’s user-land initialization, and it is the only phase where MagmaLink can be sure that all mods have been loaded and all scene builders have been filled out.

It is illegal to access any MagmaLink functionality after this phase, as the game is now in the process of finalizing its internal state and preparing to start. No MagmaLink functionality may be used at runtime due to the delicate nature of the game’s internal assumptions about its state after this point.

The Unified Load Order

If you’re here just to see when each piece of code runs, here it is:

Best Practices

To ensure a smooth modding experience for modders and users, here are some best practices to follow when working with mods in AwSW.

Users

Modders

These best practices help create a more enjoyable and stable modding experience for both users and modders alike.

Conclusion

Understanding the load order of mods in AwSW is crucial for creating, using, and maintaining mods. While it can be fun and interesting to dive deep into how mods are loaded and executed, it’s important to try to keep things simple and limit your mod’s complexity as much as possible, to ensure compatibility with other mods and future versions of the game.

This post is meant to be a starting point for understanding the intricacies of the load order, not a comprehensive guide to abusing it. With great power comes great responsibility, and modders should strive to create mods that get along with the rest of the modding ecosystem, to allow players to mix and match mods to create their ideal AwSW experience.


  1. I’ve experienced this directly in my own modding projects, where another mod unexpectedly replaced a custom asset I had added, mistakenly transforming a character into a completely different character.↩︎

  2. See https://github.com/renpy/renpy/blob/183327eec5920060af4a2db808ed19e0de4f1211/renpy/loader.py#L248↩︎

  3. See https://github.com/renpy/renpy/blob/183327eec5920060af4a2db808ed19e0de4f1211/renpy/script.py#L223↩︎

  4. See https://github.com/renpy/renpy/blob/183327eec5920060af4a2db808ed19e0de4f1211/renpy/script.py#L259↩︎

  5. See https://github.com/renpy/renpy/blob/183327eec5920060af4a2db808ed19e0de4f1211/renpy/loader.py#L183↩︎

  6. See https://github.com/renpy/renpy/blob/183327eec5920060af4a2db808ed19e0de4f1211/renpy/script.py#L701↩︎

  7. See https://github.com/renpy/renpy/blob/183327eec5920060af4a2db808ed19e0de4f1211/renpy/script.py#L266↩︎