Skip to content

Interfacing with the Submod Framework

Totally a booplicate edited this page Apr 21, 2020 · 12 revisions

With 0.11.0 came a submod framework. This allows you to register submods with dependencies and have update scripts for different versions to help keep your submods compatible with the current version of MAS.

Additionally a utility to plug functions into either labels or other functions was also implemented.

This page contains instructions on how to use both of these frameworks.

Initializing the Submod Object:

  • The Submod object is essentially initialized as a header for your submod and must be initialized in an init -990 python in mas_submod_utils block. If at any other init level, dependency checks will not run for your submod.

An example initialization is as follows:

init -990 python in mas_submod_utils:
    Submod(
        author="Monika After Story",
        name="Example Submod",
        description="This is an example submod.",
        version="0.0.1",
        dependencies={},
        settings_pane=None,
        version_updates={}
    )

The above code will initialize a Submod object with the following information:

  • Author: Monika After Story
  • Name: Example Submod
  • Description: This is an example submod.
  • Version: 0.0.1
  • Dependencies: None
  • Settings: No settings
  • Version Updates: No update scripts

NOTE: The version number must be recorded using semantic versioning and be passed in as a string.

Adding Dependencies

  • Adding dependencies is a simple process.

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 dictionary.

The format for this is as follows: "dependency submod name": ("minimum_version", "maximum_version")

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

init -990 python in mas_submod_utils:
    Submod(
        author="Monika After Story",
        name="Example Submod",
        description="This is an example submod.",
        version="0.0.1",
        dependencies={
            "Required Submod": (None, None)
        },
        settings_pane=None,
        version_updates={}
    )

Which marks our Example Submod as requiring a submod named Required Submod with no specific version range to be initialized, otherwise we cannot load MAS with this code in.

Things to note:

  • If there is no applicable minimum version and/or maximum version, they can be passed in as None.
  • 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 throw an exception and exit with a traceback, indicating 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 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()

Our Submod header would now look like:

init -990 python in mas_submod_utils:
    Submod(
        author="Monika After Story",
        name="Example Submod",
        description="This is an example submod.",
        version="0.0.1",
        dependencies={},
        settings_pane="submod_screen",
        version_updates={}
    )

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 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

Coupled with the start of this chain in the version_updates dictionary, we have a Submod init of:

init -990 python in mas_submod_utils:
    Submod(
        author="Monika After Story",
        name="Example Submod",
        description="This is an example submod.",
        version="0.0.1",
        dependencies={},
        settings_pane=None,
        version_updates={
            "monika_after_story_example_submod_v0_0_1": "monika_after_story_example_submod_v0_0_2"
        }
    )

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.txt 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.