Interfacing with the Submod Framework - Monika-After-Story/MonikaModDev GitHub Wiki

The Submod Framework is currently under a big overhaul due to MAS Python 3 migration. This page will be updated as news come.

Old-style submods will become incompatible, avoid creating new submods for now.

We will provide a tool for conversion from old-style submods to new-style submods.

Submod Framework allows you to register submods (custom mini-modifications for MAS) with dependencies and have update scripts for different versions to help keep your submods compatible with the current version of MAS.

Additionally Submod Framework provides multiple utilities for you as a developer:

  • utility to plug functions into labels or other functions
  • utility to import Python packages from a path
  • utility to dynamically load renpy modules

This page contains instructions on how to use the framework.

Defining a submod

File structure

  • You have to explicitly define your submod so MAS can detect and load it appropriately.
  • All submods must be placed within the game/Submods/ directory. Sub-folders are allowed. Valid examples:
    • game/Submods/My Submod - more outfits/
    • game/Submods/My Submod Pack/My Submod - more dialogues/
    • game/Submods/My Submod Pack/My Submod - more backgrounds/
  • All submods must include a header file with the submod definition
  • All submods must have their renpy-script in rpym and/or rpymc. Remember: rpy/rpyc are not supported and will be ignored by the submod framework
    • Submods may include assets with other file types - png, jpeg, py, txt, etc - anything you need

Header file

In the root directory of your submod, you need to place a header file - header.json, the name and extension matter! We use the JSON format for submod header, you can find more info about it online, as well as JSON validators. Here we will just cover MAS-specific information.

The most basic header file:

{
    "header_version": 1,
    "author": "Monika After Story",
    "name": "Example Submod",
    "version": "0.0.3",
    "modules": [
        "main"
    ]
}

A more advanced header file:

{
    "header_version": 1,
    "author": "Monika After Story",
    "name": "Example Submod",
    "version": "0.0.3",
    "modules": [
        "main"
    ],
    "description": "This is an example.",
    "dependencies": {
        "Some Other Submod": ["0.0.8", "0.1.5"]
    },
    "settings_pane": "example_submod_screen",
    "version_updates": {
        "monika_after_story_example_submod_v0_0_1": "monika_after_story_example_submod_v0_0_2"
    },
    "coauthors": ["Example Submod co-author"],
    "priority": 0
}

Now we will explain all the fields. The above header will initialize a submod with the following information:

  • Header version: 1
    • This field is required!
    • This is the version of MAS submod schema, in case we dramatically change the framework in the future (unlikely), we will change this number too, to avoid load submods that not yet support the new changes
    • For now keep it 1
  • Author: Monika After Story
    • This field is required!
    • Make this unique!
    • Submod author, change it to your name
  • Name: Example Submod
    • This field is required!
    • Make this unique!
    • Your submod name, change it to how you want to name your submod
  • Version: 0.0.3
    • This field is required!
    • Version of your submods, must be a string of 3 integers separated by a .
    • Change it to whatever version of your submod is, after you update your submod, raise the version as you think is appropriate
  • Modules: main
    • This field is required!
    • The modules of your submod
    • Modules are rpym/rpymc files with your code, they must be within your submod directory
    • Name of a module is being defined by its relative path and its filename without extension
    • For example: main means a main.rpym in the root directory of your submod
    • Another example: utils/logging means a logging.rpym inside a utils/ folder which is inside the root directory of your submod
  • Description: This is an example.
    • Describe your submod using this field
  • Dependencies: Some Other Submod with version from 0.0.8 to 0.1.5
    • This is an advanced feature
    • If your submod requires another submod to be installed, you can specify that using this field
    • Format: dependency_name: [min_version, max_version]
  • Settings Pane: example_submod_screen
    • This is an advanced feature
    • A screen to show in the submods menu (Esc -> Submods) under your submod name
    • This can be useful if you want to add some settings for your submod, or show extra information that doesn't fit into the description
  • Version Updates: monika_after_story_example_submod_v0_0_1 to monika_after_story_example_submod_v0_0_2
    • This is an advanced feature
    • You can specify labels to use as "update script" to run when user updates your submod
    • The labels must follow the format author_submodname_vversion (notice extra v)
    • In this example MAS will run the monika_after_story_example_submod_v0_0_1 and then monika_after_story_example_submod_v0_0_2 when the user updates
  • Co-authors: list of other people who worked on this submod
    • In this example it will be Example Submod co-author
    • This field is completely optional, you don't have to include anyone or add this field at all
  • Priority: 0
    • This is an advanced feature
    • Your submod loading priority, if you are not sure, don't specify this
    • By default all submods have 0 priority, allowed values are integers in range from -999 to 999, lower - load sooner, higher - load later
    • If your submod is some kind of a framework, you may want to load it before other submods

Adding Dependencies

Hypothetically, let's say our Example Submod submod required code from another submod (named Required Submod) to work properly.

To add this submod as a dependency for yours, we want to add a key and a value to the dependencies field. The format for this is as follows: "dependency submod name": ("minimum_version", "maximum_version")

So for our scenario here, we end up with the header:

{
    "header_version": 1,
    "author": "Monika After Story",
    "name": "Example Submod",
    "version": "0.0.3",
    "description": "This is an example.",
    "modules": [
        "main"
    ],
    "dependencies": {
        "Some Other Submod": ["0.0.8", "0.1.5"],
        "Required Submod": [null, null]
    },
    "settings_pane": "example_submod_screen",
    "version_updates": {
        "monika_after_story_example_submod_v0_0_1": "monika_after_story_example_submod_v0_0_2"
    },
    "coauthors": ["Example Submod co-author"],
    "priority": 0
}

Which marks our Example Submod as requiring a submod named Required Submod with no specific version range to be initialized, otherwise MAS will not load our submod at all until the dependency is met.

Things to note:

  • If there is no applicable minimum version and/or maximum version, they can be passed in as null.
  • Both the minimum version and maximum versions will be passed in like you passed in the version for your submod, semantic versioning as string.
  • If a dependency fails, MAS will skip your submod during the loading process and log that there is a submod which is failing the dependency.

Adding a Settings Pane:

With submods like these, it wouldn't be ideal to clutter the main menus with submod settings or ways to get to the settings for your submod. This is where the settings_pane field comes into play.

To create a settings pane, simply create a screen containing the settings for your submod. This can be done as you would for any other screen initialization in Ren'Py. To bind this to your submod, pass in the name of the screen as a string to the settings_pane field.

For example, let's say we made the following screen our settings screen:

#Don't actually name your screen like this. Use something unique
screen example_submod_screen():
    vbox:
        box_wrap False
        xfill True
        xmaximum 1000

        hbox:
            style_prefix "check"
            box_wrap False

            textbutton _("Switch setting #1") action NullAction()
            textbutton _("Switch setting #2") action NullAction()
            textbutton _("Switch setting #3") action NullAction()

You can use tooltips for buttons on your setting pane, and they will be shown on the main submod screen. The submod screen already has a tooltip defined, all we need to do is get the screen, get the tooltip and adjust its value.

This is how we do it in our example:

screen example_submod_screen():
    python:
        submods_screen = store.renpy.get_screen("submods", "screens")

        if submods_screen:
            _tooltip = submods_screen.scope.get("tooltip", None)
        else:
            _tooltip = None

    vbox:
        box_wrap False
        xfill True
        xmaximum 1000

        hbox:
            style_prefix "check"
            box_wrap False

            if _tooltip:
                textbutton _("Switch setting #1"):
                    action NullAction()
                    hovered SetField(_tooltip, "value", "You will see this text while hovering over the button")
                    unhovered SetField(_tooltip, "value", _tooltip.default)

            else:
                textbutton _("Switch setting #1"):
                    action NullAction()

It's quite a big structure, but it allows you to safely change tooltips for buttons on your screen.

As you can see, you'll need to define 2 variants for each button:

  • With the tooltip
  • Without the tooltip (as a fallback in case we fail to get the screen for some reason).

Notice that we change the tooltip via SetField. On the hovered action, we'll set the tooltip to our value, on unhovered, we'll set it back to its default value using _tooltip.default.

Creating Update scripts:

Creating update scripts is likely the most complex aspect of the submod framework.

Submod update script labels must be precisely named in order to avoid conflicts due to Submods which are named the same.

The label formatting is as follows (fields corresponding to the values in the Submod init) <author>_<name>_v<version>

Things to note:

  • Spaces in the author and name parts will be replaced with underscores
  • All characters in the label will be forced to lowercase
  • The periods in the version number will be replaced with underscores
  • The version_updates dictionary works in a "from_version": "to_version" approach, and will follow a chain that is present in the updates dictionary. (Starting from the current version number, going to the top)
  • Submod update labels must accept a parameter called version which defaults to the version the update label is for
  • Update scripts run at init 10

So with our Example Submod, the formatting will be monika_after_story_example_submod_v0_0_1(version="0.0.1") as our initial update label.

If we needed to make changes from 0.0.1 to 0.0.2 that need to be migrated, we would have the following two labels:

monika_after_story_example_submod_v0_0_1(version="v0_0_1"):
    return

monika_after_story_example_submod_v0_0_2(version="v0_0_2"):
    #Make your changes here
    return

Keeping in mind the "from_version": "to_version" approach to these updates, to continue the chain if we were to move from 0.0.2 to 0.0.3 (or any other arbitrary version), we would simply add:

"monika_after_story_example_submod_v0_0_2": "monika_after_story_example_submod_v0_0_3" after the entry we put above.

Additional Function:

  • Since there may be non-breaking changes or you may want to add a compatibility functionality for the scenario where another submod is installed, you can use the mas_submod_utils.isSubmodInstalled() function.

This function accepts two parameters, one of which is mandatory.

  • name: The name of the submod you're checking for
  • version: A minimum version number (This defaults to None. And if None, is not regarded when checking if a submod is installed)

Using Function Plugins:

While overrides are useful, they are not always ideal when it comes to maintenance over updates, or especially when it comes to only adding one or two lines to a label or function. To address this, function plugins was created

This framework allows you to register functions which can be plugged into any label you wish, and additionally other functions as well, if they are set up to run them.

Registering functions:

Registering functions is rather flexible, the parameters are as follows:

  • key - The label (or function name (as string) in which you want your function to be called in
  • _function - A pointer to the function you wish to plug-in
  • args - Arguments to supply to the function (Default: [])
  • auto_error_handling - Whether or not the function plugins framework should handle errors (will log them and not let MAS crash) or not. (Set this to False if your function performs a renpy.call or renpy.jump) (Default: True)
  • priority - The priority order in which functions should be run. These work like init levels, lower is earlier, higher is later. (For functions which renpy.call or renpy.jump, use mas_submod_utils.JUMP_CALL_PRIORITY as they should be done last) (Default: 0)

There are two ways to register functions. One way is using a decorator on the function you wish to register.

Note that the decorator approach also removes the need to manually pass in a function pointer, as it takes it directly from the function which it's attached to.

The advantage of using this approach is that it takes up fewer lines to register a function and additionally, it does better in terms of self-documentation of your code.

It's done as follows:

init python:
    @store.mas_submod_utils.functionplugin("ch30_minute")
    def example_function():
        """
        This is an example function to demonstrate registering a function plugin using the decorator approach.
        """
        renpy.say(m, "I love you!")

Which initializes a function plugin in the ch30_minute label that has Monika say "I love you!".

However, this works only if you have the function you wish to plug-in contained within your submod script files. If this is not the case, we use the second approach.

Assuming the same example_function from above:

init python:
    store.mas_submod_utils.registerFunction(
        "ch30_minute",
        example_function
    )

Which is equivalent to the previous example.

Adding Arguments:

Let's say you needed to use arguments for your function, the approach is the same, except now all we do is pass the arguments in order as a list. Let's change example_function to be this now:

init python:
    def example_function(who, what):
        """
        This is an example function to demonstrate registering a function but with arguments
        """
        renpy.say(who, what)

To make this equivalent to the last example, we would need to pass m and "I love you" in as our arguments. Both approaches can be seen demonstrated below:

Decorator: @store.mas_submod_utils.functionplugin("ch30_minute", args=[m, "I love you"])

RegisterFunction():

init python:
    store.mas_submod_utils.registerFunction(
        "ch30_minute",
        example_function,
        args=[m, "I love you!"]
    )

With args comes the following two functions:

store.mas_submod_utils.getArgs(key, _function):

  • This function allows you to get the args of a function plugin. Simply pass in the label/function it's assigned to and the function pointer itself, and it will return its args as a list to you.

store.mas_submod_utils.setArgs(key, _function, args=[]):

  • This function allows you to set the args of a function plugin. Simply pass in the label/function it's assigned to, the function pointer itself, and a list of new args.

It is also possible to unregister a plugin. Simply use the following function:

store.mas_submod_utils.unregisterFunction(key, _function):

  • This function will allow you to unregister a function plugin. Simply pass in the label/function it's assigned to and the function pointer itself, and it will no longer be mapped to that label/function.

As mentioned above, it is possible to have function plugins also be mapped to a function.

This is an obscure case but is handled regardless, however not by everything. There is no function within MAS code by default which will run function plugins. If you wish to have them run in a custom function of your own, you may do so by adding a store.mas_submod_utils.getAndRunFunctions() line.

It will automatically handle all functions plugged into your function.

Things to note:

  • Function plugins can be registered at any time after init -980
  • Function plugins by default (without auto_error_handling set to False) will automatically handle any errors in your function and log it in mas_log.log to avoid crashing MAS
  • Function plugins are run from the global store
  • Function plugins will run at the start of the label it is registered in
  • Function plugins will run if the label it is bound to is either called, jumped to, or fallen through to

Extra Globals

Thanks to the implementation of Function Plugins, there were two new global variables added:

store.mas_submod_utils.current_label:

  • This variable holds the current label we're in

store.mas_submod_utils.last_label:

  • This variable holds the last label we were in.
⚠️ **GitHub.com Fallback** ⚠️